Repository: strapi/starters-and-templates
Branch: main
Commit: 7ab5f0b7e292
Files: 204
Total size: 214.7 KB
Directory structure:
gitextract_ipi637vq/
├── .gitignore
├── LICENSE.txt
├── README.md
├── lerna.json
├── package.json
└── packages/
├── starters/
│ ├── .gitkeep
│ ├── gatsby-blog/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── starter/
│ │ │ ├── .eslintrc.js
│ │ │ ├── .gitignore
│ │ │ ├── README.md
│ │ │ ├── gatsby-browser.js
│ │ │ ├── gatsby-config.js
│ │ │ ├── gatsby-node.js
│ │ │ ├── package.json
│ │ │ ├── postcss.config.js
│ │ │ ├── src/
│ │ │ │ ├── components/
│ │ │ │ │ ├── article-card.js
│ │ │ │ │ ├── articles-grid.js
│ │ │ │ │ ├── block-media.js
│ │ │ │ │ ├── block-quote.js
│ │ │ │ │ ├── block-rich-text.js
│ │ │ │ │ ├── block-slider.js
│ │ │ │ │ ├── blocks-renderer.js
│ │ │ │ │ ├── footer.js
│ │ │ │ │ ├── headings.js
│ │ │ │ │ ├── layout.js
│ │ │ │ │ ├── navbar.js
│ │ │ │ │ └── seo.js
│ │ │ │ ├── pages/
│ │ │ │ │ ├── about.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── styles/
│ │ │ │ │ └── global.css
│ │ │ │ └── templates/
│ │ │ │ └── article-post.js
│ │ │ └── tailwind.config.js
│ │ └── starter.json
│ ├── gatsby-corporate/
│ │ └── README.md
│ ├── next-blog/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── starter/
│ │ │ ├── .eslintrc
│ │ │ ├── .prettierrc
│ │ │ ├── assets/
│ │ │ │ └── css/
│ │ │ │ └── style.css
│ │ │ ├── components/
│ │ │ │ ├── articles.js
│ │ │ │ ├── card.js
│ │ │ │ ├── image.js
│ │ │ │ ├── layout.js
│ │ │ │ ├── nav.js
│ │ │ │ └── seo.js
│ │ │ ├── lib/
│ │ │ │ ├── api.js
│ │ │ │ └── media.js
│ │ │ ├── next.config.js
│ │ │ ├── package.json
│ │ │ └── pages/
│ │ │ ├── _app.js
│ │ │ ├── _document.js
│ │ │ ├── article/
│ │ │ │ └── [slug].js
│ │ │ ├── category/
│ │ │ │ └── [slug].js
│ │ │ └── index.js
│ │ └── starter.json
│ └── next-corporate/
│ ├── README.md
│ ├── package.json
│ ├── starter/
│ │ ├── .eslintrc
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── components/
│ │ │ ├── elements/
│ │ │ │ ├── button-link.js
│ │ │ │ ├── button.js
│ │ │ │ ├── custom-link.js
│ │ │ │ ├── footer.js
│ │ │ │ ├── image.js
│ │ │ │ ├── loader.js
│ │ │ │ ├── mobile-nav-menu.js
│ │ │ │ ├── navbar.js
│ │ │ │ ├── notification-banner.js
│ │ │ │ ├── seo.js
│ │ │ │ └── video.js
│ │ │ ├── icons/
│ │ │ │ └── world.js
│ │ │ ├── layout.js
│ │ │ ├── locale-switch.js
│ │ │ ├── sections/
│ │ │ │ ├── bottom-actions.js
│ │ │ │ ├── feature-columns-group.js
│ │ │ │ ├── feature-rows-group.js
│ │ │ │ ├── hero.js
│ │ │ │ ├── large-video.js
│ │ │ │ ├── lead-form.js
│ │ │ │ ├── pricing.js
│ │ │ │ ├── rich-text.js
│ │ │ │ └── testimonials-group.js
│ │ │ └── sections.js
│ │ ├── jsconfig.json
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── pages/
│ │ │ ├── [[...slug]].js
│ │ │ ├── _app.js
│ │ │ ├── _document.js
│ │ │ └── api/
│ │ │ ├── exit-preview.js
│ │ │ └── preview.js
│ │ ├── postcss.config.js
│ │ ├── public/
│ │ │ └── .gitkeep
│ │ ├── styles/
│ │ │ └── index.css
│ │ ├── tailwind.config.js
│ │ └── utils/
│ │ ├── api.js
│ │ ├── button.js
│ │ ├── hooks.js
│ │ ├── localize.js
│ │ ├── media.js
│ │ ├── parse-cookies.js
│ │ └── types.js
│ └── starter.json
└── templates/
├── .gitkeep
├── blog/
│ ├── README.md
│ ├── package.json
│ ├── template/
│ │ ├── data/
│ │ │ └── data.json
│ │ └── src/
│ │ ├── api/
│ │ │ ├── .gitkeep
│ │ │ ├── about/
│ │ │ │ ├── content-types/
│ │ │ │ │ └── about/
│ │ │ │ │ └── schema.json
│ │ │ │ ├── controllers/
│ │ │ │ │ └── about.js
│ │ │ │ ├── routes/
│ │ │ │ │ └── about.js
│ │ │ │ └── services/
│ │ │ │ └── about.js
│ │ │ ├── article/
│ │ │ │ ├── content-types/
│ │ │ │ │ └── article/
│ │ │ │ │ └── schema.json
│ │ │ │ ├── controllers/
│ │ │ │ │ └── article.js
│ │ │ │ ├── routes/
│ │ │ │ │ └── article.js
│ │ │ │ └── services/
│ │ │ │ └── article.js
│ │ │ ├── author/
│ │ │ │ ├── content-types/
│ │ │ │ │ └── author/
│ │ │ │ │ └── schema.json
│ │ │ │ ├── controllers/
│ │ │ │ │ └── author.js
│ │ │ │ ├── routes/
│ │ │ │ │ └── author.js
│ │ │ │ └── services/
│ │ │ │ └── author.js
│ │ │ ├── category/
│ │ │ │ ├── content-types/
│ │ │ │ │ └── category/
│ │ │ │ │ └── schema.json
│ │ │ │ ├── controllers/
│ │ │ │ │ └── category.js
│ │ │ │ ├── routes/
│ │ │ │ │ └── category.js
│ │ │ │ └── services/
│ │ │ │ └── category.js
│ │ │ └── global/
│ │ │ ├── content-types/
│ │ │ │ └── global/
│ │ │ │ └── schema.json
│ │ │ ├── controllers/
│ │ │ │ └── global.js
│ │ │ ├── routes/
│ │ │ │ └── global.js
│ │ │ └── services/
│ │ │ └── global.js
│ │ ├── bootstrap.js
│ │ ├── components/
│ │ │ └── shared/
│ │ │ ├── media.json
│ │ │ ├── quote.json
│ │ │ ├── rich-text.json
│ │ │ ├── seo.json
│ │ │ └── slider.json
│ │ ├── extensions/
│ │ │ └── .gitkeep
│ │ └── index.js
│ └── template.json
├── corporate/
│ ├── README.md
│ ├── package.json
│ ├── template/
│ │ ├── data/
│ │ │ ├── data.js
│ │ │ ├── en/
│ │ │ │ ├── global.json
│ │ │ │ ├── index.js
│ │ │ │ └── pages.json
│ │ │ ├── fr/
│ │ │ │ ├── global.json
│ │ │ │ ├── index.js
│ │ │ │ └── pages.json
│ │ │ └── lead-form-submissions.json
│ │ └── src/
│ │ ├── api/
│ │ │ ├── .gitkeep
│ │ │ ├── global/
│ │ │ │ ├── content-types/
│ │ │ │ │ └── global/
│ │ │ │ │ └── schema.json
│ │ │ │ ├── controllers/
│ │ │ │ │ └── global.js
│ │ │ │ ├── routes/
│ │ │ │ │ └── global.js
│ │ │ │ └── services/
│ │ │ │ └── global.js
│ │ │ ├── lead-form-submission/
│ │ │ │ ├── content-types/
│ │ │ │ │ └── lead-form-submission/
│ │ │ │ │ └── schema.json
│ │ │ │ ├── controllers/
│ │ │ │ │ └── lead-form-submission.js
│ │ │ │ ├── routes/
│ │ │ │ │ └── lead-form-submission.js
│ │ │ │ └── services/
│ │ │ │ └── lead-form-submission.js
│ │ │ └── page/
│ │ │ ├── content-types/
│ │ │ │ └── page/
│ │ │ │ └── schema.json
│ │ │ ├── controllers/
│ │ │ │ └── page.js
│ │ │ ├── routes/
│ │ │ │ └── page.js
│ │ │ └── services/
│ │ │ └── page.js
│ │ ├── bootstrap.js
│ │ ├── components/
│ │ │ ├── elements/
│ │ │ │ ├── feature-column.json
│ │ │ │ ├── feature-row.json
│ │ │ │ ├── feature.json
│ │ │ │ ├── footer-section.json
│ │ │ │ ├── logos.json
│ │ │ │ ├── notification-banner.json
│ │ │ │ ├── plan.json
│ │ │ │ └── testimonial.json
│ │ │ ├── layout/
│ │ │ │ ├── footer.json
│ │ │ │ └── navbar.json
│ │ │ ├── links/
│ │ │ │ ├── button-link.json
│ │ │ │ ├── button.json
│ │ │ │ └── link.json
│ │ │ ├── meta/
│ │ │ │ └── metadata.json
│ │ │ └── sections/
│ │ │ ├── bottom-actions.json
│ │ │ ├── feature-columns-group.json
│ │ │ ├── feature-rows-group.json
│ │ │ ├── hero.json
│ │ │ ├── large-video.json
│ │ │ ├── lead-form.json
│ │ │ ├── pricing.json
│ │ │ ├── rich-text.json
│ │ │ └── testimonials-group.json
│ │ └── index.js
│ └── template.json
└── ecommerce/
├── README.md
├── package.json
├── template/
│ ├── data/
│ │ └── data.js
│ └── src/
│ ├── api/
│ │ ├── .gitkeep
│ │ ├── category/
│ │ │ ├── content-types/
│ │ │ │ └── category/
│ │ │ │ └── schema.json
│ │ │ ├── controllers/
│ │ │ │ └── category.js
│ │ │ ├── routes/
│ │ │ │ └── category.js
│ │ │ └── services/
│ │ │ └── category.js
│ │ └── product/
│ │ ├── content-types/
│ │ │ └── product/
│ │ │ └── schema.json
│ │ ├── controllers/
│ │ │ └── product.js
│ │ ├── routes/
│ │ │ └── product.js
│ │ └── services/
│ │ └── product.js
│ ├── bootstrap.js
│ ├── components/
│ │ └── custom/
│ │ └── custom-field.json
│ └── index.js
└── template.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,windows,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,windows,visualstudiocode
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env.production
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### VisualStudioCode ###
.vscode/*
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/node,macos,windows,visualstudiocode
# Template development directory
template-dev/
================================================
FILE: LICENSE.txt
================================================
MIT License
Copyright (c) 2021 Strapi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
> [!WARNING]
> This repository is only compatible with Strapi v4 version
> The most recent information can be found at https://docs.strapi.io/
# Strapi starters & templates
Monorepo for all official Strapi v4 starters and templates.
### Starters
> As you may have [noticed](https://github.com/strapi/starters-and-templates/issues?q=is%3Aissue+is%3Aopen+install), a lot of the Starters are out of date and given constraints in bandwidth and other priorities, we've decided to sunset all Starters and only actively maintain a single Next.js Starter.
- [Gatsby Blog](./packages/starters/gatsby-blog)
- [Next Blog](./packages/starters/next-blog)
- [Next Corporate Site](./packages/starters/next-corporate)
### Templates
- [Blog](./packages/templates/blog)
- [Corporate](./packages/templates/corporate)
- [Ecommerce](./packages/templates/ecommerce)
================================================
FILE: lerna.json
================================================
{
"packages": [
"packages/templates/*",
"packages/starters/*"
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "independent"
}
================================================
FILE: package.json
================================================
{
"name": "@strapi/starters-and-templates",
"version": "1.0.0",
"private": true,
"description": "All Strapi templates",
"repository": "https://github.com/strapi/starters-and-templates.git",
"license": "MIT",
"author": {
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
},
"maintainers": [
{
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
}
],
"workspaces": [
"packages/**/*"
],
"devDependencies": {
"lerna": "^4.0.0"
}
}
================================================
FILE: packages/starters/.gitkeep
================================================
================================================
FILE: packages/starters/gatsby-blog/README.md
================================================
# Strapi Starter Gatsby Blog
Gatsby starter for creating a blog with Strapi.
This starter allows you to try Strapi with Gatsby with the example of a simple blog. It is fully customizable and due to the fact that it is open source, fully open to contributions. So do not hesitate to add new features and report bugs!
This starter uses the [Strapi blog template](https://github.com/strapi/starters-and-templates/tree/main/packages/templates/blog)
## Getting started
Use our `create-strapi-starter` CLI to create your project.
```sh
# Using Yarn
yarn create strapi-starter my-project gatsby-blog
# Or using NPM
npx create-strapi-starter my-project gatsby-blog
```
The CLI will create a monorepo, install dependencies, and run your project automatically.
The Gatsby frontend server will run here => [http://localhost:3000](http://localhost:3000)
The Strapi backend server will run here => [http://localhost:1337](http://localhost:1337)
You will however need to manually create a full access [API token](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/configurations/optional/api-tokens.html) in Strapi. Once it's created, save it as `STRAPI_TOKEN` in your environment variables.
## Deploying to production
You will need to deploy the `frontend` and `backend` projects separately. Here are the docs to deploy each one:
- [Deploy Strapi](https://strapi.io/documentation/developer-docs/latest/setup-deployment-guides/deployment.html#hosting-provider-guides)
- [Deploy Gatsby](https://www.gatsbyjs.com/docs/deploying-and-hosting/)
Enjoy this starter!
================================================
FILE: packages/starters/gatsby-blog/package.json
================================================
{
"name": "@strapi/starter-gatsby-blog",
"version": "1.0.7",
"description": "Strapi blog starter with Gatsby",
"keywords": [
"strapi",
"starter",
"gatsby",
"blog"
],
"homepage": "https://github.com/strapi/starters-and-templates/tree/main/packages/starters/gatsby-blog#readme",
"bugs": {
"url": "https://github.com/strapi/starters-and-templates/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/strapi/starters-and-templates.git"
},
"license": "MIT",
"author": {
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
},
"maintainers": [
{
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
}
]
}
================================================
FILE: packages/starters/gatsby-blog/starter/.eslintrc.js
================================================
module.exports = {
globals: {
__PATH_PREFIX__: true,
},
extends: ["react-app", "prettier"],
plugins: ["prettier"],
rules: {
"prettier/prettier": [
"error",
{
printWidth: 80,
singleQuote: false,
trailingComma: "es5",
semi: false,
tabWidth: 2,
},
],
},
}
================================================
FILE: packages/starters/gatsby-blog/starter/.gitignore
================================================
node_modules/
.cache/
public
.env
.env.production
.env.local
.env.development
================================================
FILE: packages/starters/gatsby-blog/starter/README.md
================================================
Gatsby minimal starter
## 🚀 Quick start
1. **Create a Gatsby site.**
Use the Gatsby CLI to create a new site, specifying the minimal starter.
```shell
# create a new Gatsby site using the minimal starter
npm init gatsby
```
2. **Start developing.**
Navigate into your new site’s directory and start it up.
```shell
cd my-gatsby-site/
npm run develop
```
3. **Open the code and start customizing!**
Your site is now running at http://localhost:8000!
Edit `src/pages/index.js` to see your site update in real-time!
4. **Learn more**
- [Documentation](https://www.gatsbyjs.com/docs/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter)
- [Tutorials](https://www.gatsbyjs.com/tutorial/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter)
- [Guides](https://www.gatsbyjs.com/tutorial/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter)
- [API Reference](https://www.gatsbyjs.com/docs/api-reference/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter)
- [Plugin Library](https://www.gatsbyjs.com/plugins?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter)
- [Cheat Sheet](https://www.gatsbyjs.com/docs/cheat-sheet/?utm_source=starter&utm_medium=readme&utm_campaign=minimal-starter)
## 🚀 Quick start (Gatsby Cloud)
Deploy this starter with one click on [Gatsby Cloud](https://www.gatsbyjs.com/cloud/):
[ ](https://www.gatsbyjs.com/dashboard/deploynow?url=https://github.com/gatsbyjs/gatsby-starter-minimal)
================================================
FILE: packages/starters/gatsby-blog/starter/gatsby-browser.js
================================================
import "./src/styles/global.css"
================================================
FILE: packages/starters/gatsby-blog/starter/gatsby-config.js
================================================
require("dotenv").config({
path: `.env.${process.env.NODE_ENV}`,
})
module.exports = {
plugins: [
"gatsby-plugin-gatsby-cloud",
"gatsby-plugin-postcss",
{
resolve: "gatsby-source-strapi",
options: {
apiURL: process.env.STRAPI_API_URL || "http://localhost:1337",
accessToken: process.env.STRAPI_TOKEN,
collectionTypes: [
{
singularName: "article",
queryParams: {
publicationState:
process.env.GATSBY_IS_PREVIEW === "true" ? "preview" : "live",
populate: {
cover: "*",
blocks: {
populate: "*",
},
},
},
},
{
singularName: "author",
},
{
singularName: "category",
},
],
singleTypes: [
{
singularName: "about",
queryParams: {
populate: {
blocks: {
populate: "*",
},
},
},
},
{
singularName: "global",
queryParams: {
populate: {
favicon: "*",
defaultSeo: {
populate: "*",
},
},
},
},
],
},
},
"gatsby-plugin-image",
"gatsby-plugin-sharp",
"gatsby-transformer-sharp",
"gatsby-transformer-remark",
],
}
================================================
FILE: packages/starters/gatsby-blog/starter/gatsby-node.js
================================================
const path = require("path")
exports.createPages = async ({ graphql, actions, reporter }) => {
const { createPage } = actions
// Define a template for blog post
const articlePost = path.resolve("./src/templates/article-post.js")
const result = await graphql(
`
{
allStrapiArticle {
nodes {
title
slug
}
}
}
`
)
if (result.errors) {
reporter.panicOnBuild(
`There was an error loading your Strapi articles`,
result.errors
)
return
}
const articles = result.data.allStrapiArticle.nodes
if (articles.length > 0) {
articles.forEach((article) => {
createPage({
path: `/article/${article.slug}`,
component: articlePost,
context: {
slug: article.slug,
},
})
})
}
}
================================================
FILE: packages/starters/gatsby-blog/starter/package.json
================================================
{
"name": "my-gatsby-blog",
"version": "1.0.7",
"private": true,
"description": "Strapi Gatsby Blog",
"author": "Strapi team",
"keywords": [
"gatsby"
],
"scripts": {
"develop": "gatsby develop",
"start": "gatsby develop",
"build": "gatsby build",
"serve": "gatsby serve",
"clean": "gatsby clean",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"gatsby": "^4.6.1",
"gatsby-plugin-gatsby-cloud": "^4.7.0",
"gatsby-plugin-image": "^2.6.0",
"gatsby-plugin-postcss": "^5.6.0",
"gatsby-plugin-sharp": "^4.6.0",
"gatsby-source-strapi": "^2.0.0",
"gatsby-transformer-remark": "^5.6.0",
"gatsby-transformer-sharp": "^4.6.0",
"postcss": "^8.4.6",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-helmet": "^6.1.0",
"react-slick": "^0.28.1",
"slick-carousel": "^1.8.1"
},
"devDependencies": {
"@tailwindcss/line-clamp": "^0.3.1",
"@tailwindcss/typography": "^0.5.1",
"autoprefixer": "^10.4.2",
"eslint": "^8.8.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-react-app": "^7.0.0",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.5.1",
"prettier-plugin-tailwindcss": "^0.1.4",
"tailwindcss": "^3.0.18"
}
}
================================================
FILE: packages/starters/gatsby-blog/starter/postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
================================================
FILE: packages/starters/gatsby-blog/starter/src/components/article-card.js
================================================
import React from "react"
import { Link, graphql } from "gatsby"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
const ArticleCard = ({ article }) => {
return (
{article.title}
{article.description}
)
}
export const query = graphql`
fragment ArticleCard on STRAPI_ARTICLE {
id
slug
title
description
cover {
alternativeText
localFile {
childImageSharp {
gatsbyImageData(aspectRatio: 1.77)
}
}
}
}
`
export default ArticleCard
================================================
FILE: packages/starters/gatsby-blog/starter/src/components/articles-grid.js
================================================
import React from "react"
import ArticleCard from "./article-card"
const ArticlesGrid = ({ articles }) => {
return (
{articles.map((article) => (
))}
)
}
export default ArticlesGrid
================================================
FILE: packages/starters/gatsby-blog/starter/src/components/block-media.js
================================================
import React from "react"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
const BlockMedia = ({ data }) => {
const isVideo = data.file.mime.startsWith("video")
return (
{isVideo ? (
TODO video
) : (
)}
)
}
export default BlockMedia
================================================
FILE: packages/starters/gatsby-blog/starter/src/components/block-quote.js
================================================
import React from "react"
const BlockQuote = ({ data }) => {
return (
{data.quoteBody}
{data.title}
)
}
export default BlockQuote
================================================
FILE: packages/starters/gatsby-blog/starter/src/components/block-rich-text.js
================================================
import React from "react"
const BlockRichText = ({ data }) => {
return (
)
}
export default BlockRichText
================================================
FILE: packages/starters/gatsby-blog/starter/src/components/block-slider.js
================================================
import React from "react"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
import Slider from "react-slick"
import "slick-carousel/slick/slick.css"
import "slick-carousel/slick/slick-theme.css"
const BlockSlider = ({ data }) => {
return (
{data.files.map((file) => (
))}
)
}
export default BlockSlider
================================================
FILE: packages/starters/gatsby-blog/starter/src/components/blocks-renderer.js
================================================
import React from "react"
import { graphql } from "gatsby"
import BlockRichText from "./block-rich-text"
import BlockMedia from "./block-media"
import BlockQuote from "./block-quote"
import BlockSlider from "./block-slider"
const componentsMap = {
STRAPI__COMPONENT_SHARED_RICH_TEXT: BlockRichText,
STRAPI__COMPONENT_SHARED_MEDIA: BlockMedia,
STRAPI__COMPONENT_SHARED_QUOTE: BlockQuote,
STRAPI__COMPONENT_SHARED_SLIDER: BlockSlider,
}
const Block = ({ block }) => {
const Component = componentsMap[block.__typename]
if (!Component) {
return null
}
return
}
const BlocksRenderer = ({ blocks }) => {
return (
{blocks.map((block, index) => (
))}
)
}
export const query = graphql`
fragment Blocks on STRAPI__COMPONENT_SHARED_MEDIASTRAPI__COMPONENT_SHARED_QUOTESTRAPI__COMPONENT_SHARED_RICH_TEXTSTRAPI__COMPONENT_SHARED_SLIDERUnion {
__typename
... on STRAPI__COMPONENT_SHARED_RICH_TEXT {
richTextBody: body {
__typename
data {
id
childMarkdownRemark {
html
}
}
}
}
... on STRAPI__COMPONENT_SHARED_MEDIA {
file {
mime
localFile {
childImageSharp {
gatsbyImageData
}
}
}
}
... on STRAPI__COMPONENT_SHARED_QUOTE {
title
quoteBody: body
}
... on STRAPI__COMPONENT_SHARED_SLIDER {
files {
id
mime
localFile {
childImageSharp {
gatsbyImageData
}
}
}
}
}
`
export default BlocksRenderer
================================================
FILE: packages/starters/gatsby-blog/starter/src/components/footer.js
================================================
import React from "react"
const Footer = () => {
const currentYear = new Date().getFullYear()
return (
)
}
export default Footer
================================================
FILE: packages/starters/gatsby-blog/starter/src/components/headings.js
================================================
import React from "react"
const Headings = ({ title, description }) => {
return (
{title}
{description && (
{description}
)}
)
}
export default Headings
================================================
FILE: packages/starters/gatsby-blog/starter/src/components/layout.js
================================================
import React from "react"
import Footer from "./footer"
import Navbar from "./navbar"
const Layout = ({ children }) => {
return (
)
}
export default Layout
================================================
FILE: packages/starters/gatsby-blog/starter/src/components/navbar.js
================================================
import { Link } from "gatsby"
import React from "react"
const Navbar = () => {
return (
)
}
export default Navbar
================================================
FILE: packages/starters/gatsby-blog/starter/src/components/seo.js
================================================
import React from "react"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"
const Seo = ({ seo = {} }) => {
const { strapiGlobal } = useStaticQuery(graphql`
query {
strapiGlobal {
siteName
favicon {
localFile {
url
}
}
defaultSeo {
metaTitle
metaDescription
shareImage {
localFile {
url
}
}
}
}
}
`)
const { siteName, defaultSeo, favicon } = strapiGlobal
// Merge default and page-specific SEO values
const fullSeo = { ...defaultSeo, ...seo }
// Add site name to title
fullSeo.metaTitle = `${fullSeo.metaTitle} | ${siteName}`
const getMetaTags = () => {
const tags = []
if (fullSeo.metaTitle) {
tags.push(
{
property: "og:title",
content: fullSeo.metaTitle,
},
{
name: "twitter:title",
content: fullSeo.metaTitle,
}
)
}
if (fullSeo.metaDescription) {
tags.push(
{
name: "description",
content: fullSeo.metaDescription,
},
{
property: "og:description",
content: fullSeo.metaDescription,
},
{
name: "twitter:description",
content: fullSeo.metaDescription,
}
)
}
if (fullSeo.shareImage) {
const imageUrl = fullSeo.shareImage.localFile.url
tags.push(
{
name: "image",
content: imageUrl,
},
{
property: "og:image",
content: imageUrl,
},
{
name: "twitter:image",
content: imageUrl,
}
)
}
if (fullSeo.article) {
tags.push({
property: "og:type",
content: "article",
})
}
tags.push({ name: "twitter:card", content: "summary_large_image" })
return tags
}
const metaTags = getMetaTags()
return (
)
}
export default Seo
================================================
FILE: packages/starters/gatsby-blog/starter/src/pages/about.js
================================================
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Layout from "../components/layout"
import Seo from "../components/seo"
import BlocksRenderer from "../components/blocks-renderer"
import Headings from "../components/headings"
const AboutPage = () => {
const { strapiAbout } = useStaticQuery(graphql`
query {
strapiAbout {
title
blocks {
...Blocks
}
}
}
`)
const { title, blocks } = strapiAbout
const seo = {
metaTitle: title,
metaDescription: title,
}
return (
)
}
export default AboutPage
================================================
FILE: packages/starters/gatsby-blog/starter/src/pages/index.js
================================================
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Layout from "../components/layout"
import ArticlesGrid from "../components/articles-grid"
import Seo from "../components/seo"
import Headings from "../components/headings"
const IndexPage = () => {
const { allStrapiArticle, strapiGlobal } = useStaticQuery(graphql`
query {
allStrapiArticle {
nodes {
...ArticleCard
}
}
strapiGlobal {
siteName
siteDescription
}
}
`)
return (
)
}
export default IndexPage
================================================
FILE: packages/starters/gatsby-blog/starter/src/styles/global.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
================================================
FILE: packages/starters/gatsby-blog/starter/src/templates/article-post.js
================================================
import React from "react"
import { graphql } from "gatsby"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
import Layout from "../components/layout"
import BlocksRenderer from "../components/blocks-renderer"
import Seo from "../components/seo"
const ArticlePage = ({ data }) => {
const article = data.strapiArticle
const seo = {
metaTitle: article.title,
metaDescription: article.description,
shareImage: article.cover,
}
return (
{article.title}
{article.description}
)
}
export const pageQuery = graphql`
query ($slug: String) {
strapiArticle(slug: { eq: $slug }) {
id
slug
title
description
blocks {
...Blocks
}
cover {
alternativeText
localFile {
url
childImageSharp {
gatsbyImageData
}
}
}
}
}
`
export default ArticlePage
================================================
FILE: packages/starters/gatsby-blog/starter/tailwind.config.js
================================================
const colors = require("tailwindcss/colors")
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {
colors: {
neutral: colors.neutral,
primary: colors.sky,
},
},
container: {
center: true,
padding: {
DEFAULT: "1rem",
xs: "1rem",
sm: "2rem",
xl: "5rem",
"2xl": "6rem",
},
},
},
plugins: [
require("@tailwindcss/line-clamp"),
require("@tailwindcss/typography"),
],
}
================================================
FILE: packages/starters/gatsby-blog/starter.json
================================================
{
"template": {
"name": "@strapi/template-blog",
"version": "^2.0.0"
}
}
================================================
FILE: packages/starters/gatsby-corporate/README.md
================================================
# Placeholder for Strapi Gatsby corporate starter
The Strapi team will build this starter soon.
In the meantime, you can check out our other Strapi v4 starters:
- [Gatsby Blog](https://github.com/strapi/starters-and-templates/tree/main/packages/starters/gatsby-blog)
- [Next Blog](https://github.com/strapi/starters-and-templates/tree/main/packages/starters/next-blog)
- [Next Corporate Site](https://github.com/strapi/starters-and-templates/tree/main/packages/starters/next-corporate)
================================================
FILE: packages/starters/next-blog/README.md
================================================
# Strapi Starter Next Blog
Next starter for creating a blog with Strapi.

This starter allows you to try Strapi with Next with the example of a simple blog. It is fully customizable and due to the fact that it is open source, fully open to contributions. So do not hesitate to add new features and report bugs!
This starter uses the [Strapi blog template](https://github.com/strapi/strapi-template-blog)
Check out all of our starters [here](https://strapi.io/starters)
## Features
- 2 Content types: Article, Category
- 2 Created articles
- 3 Created categories
- Responsive design using UIkit
Pages:
- "/" to display every articles
- "/article/:id" to display one article
- "/category/:id" display articles depending on the category
## Getting started
Use our `create-strapi-starter` CLI to create your project.
```sh
# Using Yarn
yarn create strapi-starter my-project next-blog
# Or using NPM
npx create-strapi-starter my-project next-blog
```
The CLI will create a monorepo, install dependencies, and run your project automatically.
The Next frontend server will run here => [http://localhost:3000](http://localhost:3000)
The Strapi backend server will run here => [http://localhost:1337](http://localhost:1337)
## Deploying to production
You will need to deploy the `frontend` and `backend` projects separately. Here are the docs to deploy each one:
- [Deploy Strapi](https://strapi.io/documentation/developer-docs/latest/setup-deployment-guides/deployment.html#hosting-provider-guides)
- [Deploy Next](https://nextjs.org/docs/deployment)
Don't forget to setup the environment variables on your production app:
For the frontend the following environment variable is required:
- `NEXT_PUBLIC_STRAPI_API_URL`: URL of your Strapi backend, without trailing slash
Enjoy this starter!
================================================
FILE: packages/starters/next-blog/package.json
================================================
{
"name": "@strapi/starter-next-blog",
"version": "1.0.4",
"description": "Strapi blog starter with Next.js",
"keywords": [
"strapi",
"starter",
"nextjs",
"blog"
],
"homepage": "https://github.com/strapi/starters-and-templates/tree/main/packages/starters/next-blog#readme",
"bugs": {
"url": "https://github.com/strapi/starters-and-templates/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/strapi/starters-and-templates.git"
},
"license": "MIT",
"author": {
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
},
"maintainers": [
{
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
}
]
}
================================================
FILE: packages/starters/next-blog/starter/.eslintrc
================================================
{
"extends": [
"next",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": "error"
}
}
================================================
FILE: packages/starters/next-blog/starter/.prettierrc
================================================
{
"printWidth": 80,
"singleQuote": false,
"trailingComma": "es5",
"semi": false,
"tabWidth": 2
}
================================================
FILE: packages/starters/next-blog/starter/assets/css/style.css
================================================
a {
text-decoration: none;
}
h1 {
font-family: Staatliches;
font-size: 120px;
}
#category {
font-family: Staatliches;
font-weight: 500;
}
#title {
letter-spacing: 0.4px;
font-size: 22px;
font-size: 1.375rem;
line-height: 1.13636;
}
#banner {
margin: 20px;
height: 800px;
}
#editor {
font-size: 16px;
font-size: 1rem;
line-height: 1.75;
}
.uk-navbar-container {
background: #fff !important;
font-family: Staatliches;
}
img:hover {
opacity: 1;
transition: opacity 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);
}
================================================
FILE: packages/starters/next-blog/starter/components/articles.js
================================================
import React from "react"
import Card from "./card"
const Articles = ({ articles }) => {
const leftArticlesCount = Math.ceil(articles.length / 5)
const leftArticles = articles.slice(0, leftArticlesCount)
const rightArticles = articles.slice(leftArticlesCount, articles.length)
return (
{leftArticles.map((article, i) => {
return (
)
})}
{rightArticles.map((article, i) => {
return (
)
})}
)
}
export default Articles
================================================
FILE: packages/starters/next-blog/starter/components/card.js
================================================
import React from "react"
import Link from "next/link"
import NextImage from "./image"
const Card = ({ article }) => {
return (
{article.attributes.category.name}
{article.attributes.title}
)
}
export default Card
================================================
FILE: packages/starters/next-blog/starter/components/image.js
================================================
import { getStrapiMedia } from "../lib/media"
import NextImage from "next/image"
const Image = ({ image, style }) => {
const { url, alternativeText, width, height } = image.data.attributes
// const loader = () => {
// return getStrapiMedia(image)
// }
return (
)
}
export default Image
================================================
FILE: packages/starters/next-blog/starter/components/layout.js
================================================
import Nav from "./nav"
const Layout = ({ children, categories, seo }) => (
<>
{children}
>
)
export default Layout
================================================
FILE: packages/starters/next-blog/starter/components/nav.js
================================================
import React from "react"
import Link from "next/link"
const Nav = ({ categories }) => {
return (
)
}
export default Nav
================================================
FILE: packages/starters/next-blog/starter/components/seo.js
================================================
import Head from "next/head"
import { useContext } from "react"
import { GlobalContext } from "../pages/_app"
import { getStrapiMedia } from "../lib/media"
const Seo = ({ seo }) => {
const { defaultSeo, siteName } = useContext(GlobalContext)
const seoWithDefaults = {
...defaultSeo,
...seo,
}
const fullSeo = {
...seoWithDefaults,
// Add title suffix
metaTitle: `${seoWithDefaults.metaTitle} | ${siteName}`,
// Get full image URL
shareImage: getStrapiMedia(seoWithDefaults.shareImage),
}
return (
{fullSeo.metaTitle && (
<>
{fullSeo.metaTitle}
>
)}
{fullSeo.metaDescription && (
<>
>
)}
{fullSeo.shareImage && (
<>
>
)}
{fullSeo.article && }
)
}
export default Seo
================================================
FILE: packages/starters/next-blog/starter/lib/api.js
================================================
import qs from "qs"
/**
* Get full Strapi URL from path
* @param {string} path Path of the URL
* @returns {string} Full Strapi URL
*/
export function getStrapiURL(path = "") {
return `${
process.env.NEXT_PUBLIC_STRAPI_API_URL || "http://localhost:1337"
}${path}`
}
/**
* Helper to make GET requests to Strapi API endpoints
* @param {string} path Path of the API route
* @param {Object} urlParamsObject URL params object, will be stringified
* @param {Object} options Options passed to fetch
* @returns Parsed API call response
*/
export async function fetchAPI(path, urlParamsObject = {}, options = {}) {
// Merge default and user options
const mergedOptions = {
headers: {
"Content-Type": "application/json",
},
...options,
}
// Build request URL
const queryString = qs.stringify(urlParamsObject)
const requestUrl = `${getStrapiURL(
`/api${path}${queryString ? `?${queryString}` : ""}`
)}`
// Trigger API call
const response = await fetch(requestUrl, mergedOptions)
// Handle response
if (!response.ok) {
console.error(response.statusText)
throw new Error(`An error occurred please try again`)
}
const data = await response.json()
return data
}
================================================
FILE: packages/starters/next-blog/starter/lib/media.js
================================================
import { getStrapiURL } from "./api"
export function getStrapiMedia(media) {
const { url } = media.data.attributes
const imageUrl = url.startsWith("/") ? getStrapiURL(url) : url
return imageUrl
}
================================================
FILE: packages/starters/next-blog/starter/next.config.js
================================================
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
images: {
loader: "default",
domains: ["localhost"],
},
}
module.exports = nextConfig
================================================
FILE: packages/starters/next-blog/starter/package.json
================================================
{
"name": "my-next-blog",
"version": "1.0.3",
"private": true,
"scripts": {
"develop": "next dev",
"dev": "next dev",
"build": "next build",
"start": "next start",
"deploy": "next build && next export",
"lint": "next lint",
"lint:fix": "next lint --fix"
},
"dependencies": {
"moment": "^2.24.0",
"next": "^11.0.0",
"qs": "^6.10.1",
"react": "17.0.0",
"react-dom": "17.0.0",
"react-markdown": "^4.2.2",
"react-moment": "^0.9.6"
},
"license": "MIT",
"devDependencies": {
"eslint": "^7.30.0",
"eslint-config-next": "^11.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"prettier": "^2.3.1"
}
}
================================================
FILE: packages/starters/next-blog/starter/pages/_app.js
================================================
import App from "next/app"
import Head from "next/head"
import "../assets/css/style.css"
import { createContext } from "react"
import { fetchAPI } from "../lib/api"
import { getStrapiMedia } from "../lib/media"
// Store Strapi Global object in context
export const GlobalContext = createContext({})
const MyApp = ({ Component, pageProps }) => {
const { global } = pageProps
return (
<>
>
)
}
// getInitialProps disables automatic static optimization for pages that don't
// have getStaticProps. So article, category and home pages still get SSG.
// Hopefully we can replace this with getStaticProps once this issue is fixed:
// https://github.com/vercel/next.js/discussions/10949
MyApp.getInitialProps = async (ctx) => {
// Calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(ctx)
// Fetch global site settings from Strapi
const globalRes = await fetchAPI("/global", {
populate: {
favicon: "*",
defaultSeo: {
populate: "*",
},
},
})
// Pass the data to our page via props
return { ...appProps, pageProps: { global: globalRes.data } }
}
export default MyApp
================================================
FILE: packages/starters/next-blog/starter/pages/_document.js
================================================
import Document, { Html, Head, Main, NextScript } from "next/document"
class MyDocument extends Document {
render() {
return (
{/* eslint-disable-next-line */}
)
}
}
export default MyDocument
================================================
FILE: packages/starters/next-blog/starter/pages/article/[slug].js
================================================
import ReactMarkdown from "react-markdown"
import Moment from "react-moment"
import { fetchAPI } from "../../lib/api"
import Layout from "../../components/layout"
import NextImage from "../../components/image"
import Seo from "../../components/seo"
import { getStrapiMedia } from "../../lib/media"
const Article = ({ article, categories }) => {
const imageUrl = getStrapiMedia(article.attributes.image)
const seo = {
metaTitle: article.attributes.title,
metaDescription: article.attributes.description,
shareImage: article.attributes.image,
article: true,
}
return (
{article.attributes.title}
{article.attributes.author.picture && (
)}
By {article.attributes.author.name}
{article.attributes.published_at}
)
}
export async function getStaticPaths() {
const articlesRes = await fetchAPI("/articles", { fields: ["slug"] })
return {
paths: articlesRes.data.map((article) => ({
params: {
slug: article.attributes.slug,
},
})),
fallback: false,
}
}
export async function getStaticProps({ params }) {
const articlesRes = await fetchAPI("/articles", {
filters: {
slug: params.slug,
},
populate: "*",
})
const categoriesRes = await fetchAPI("/categories")
return {
props: { article: articlesRes.data[0], categories: categoriesRes },
revalidate: 1,
}
}
export default Article
================================================
FILE: packages/starters/next-blog/starter/pages/category/[slug].js
================================================
import Articles from "../../components/articles"
import { fetchAPI } from "../../lib/api"
import Layout from "../../components/layout"
import Seo from "../../components/seo"
const Category = ({ category, categories }) => {
const seo = {
metaTitle: category.attributes.name,
metaDescription: `All ${category.attributes.name} articles`,
}
return (
{category.attributes.name}
)
}
export async function getStaticPaths() {
const categoriesRes = await fetchAPI("/categories", { fields: ["slug"] })
return {
paths: categoriesRes.data.map((category) => ({
params: {
slug: category.attributes.slug,
},
})),
fallback: false,
}
}
export async function getStaticProps({ params }) {
const matchingCategories = await fetchAPI("/categories", {
filters: { slug: params.slug },
populate: {
articles: {
populate: "*",
},
},
})
const allCategories = await fetchAPI("/categories")
return {
props: {
category: matchingCategories.data[0],
categories: allCategories,
},
revalidate: 1,
}
}
export default Category
================================================
FILE: packages/starters/next-blog/starter/pages/index.js
================================================
import React from "react"
import Articles from "../components/articles"
import Layout from "../components/layout"
import Seo from "../components/seo"
import { fetchAPI } from "../lib/api"
const Home = ({ articles, categories, homepage }) => {
return (
{homepage.attributes.hero.title}
)
}
export async function getStaticProps() {
// Run API calls in parallel
const [articlesRes, categoriesRes, homepageRes] = await Promise.all([
fetchAPI("/articles", { populate: "*" }),
fetchAPI("/categories", { populate: "*" }),
fetchAPI("/homepage", {
populate: {
hero: "*",
seo: { populate: "*" },
},
}),
])
return {
props: {
articles: articlesRes.data,
categories: categoriesRes.data,
homepage: homepageRes.data,
},
revalidate: 1,
}
}
export default Home
================================================
FILE: packages/starters/next-blog/starter.json
================================================
{
"template": {
"name": "@strapi/template-blog",
"version": "^1.0.0"
}
}
================================================
FILE: packages/starters/next-corporate/README.md
================================================
# Strapi Starter Next Corporate Site
Next starter for creating a corporate site with Strapi.
[View the live demo](https://strapi-starter-next-corporate.vercel.app/) • [Read the blog post](https://strapi.io/blog/strapi-starter-next-corporate-site)

This starter is designed for flexibility. Using it, you'll be able to manage your website content entirely in Strapi, and get a Next app automatically generated. Marketing teams will be able to create pages and design their layout without help from developers.
This starter features:
- Pages creation within Strapi, no code necessary
- Fully flexible page structure: design the pages you want using UI Sections
- 8 UI Sections out of the box: Hero, RichText, LargeVideo, Testimonials, Pricing, BottomActions, FeatureRows, FeatureColumns
- Easy to theme with Tailwind
- Static site generation with Next
- An integrated Preview Mode, to view your pages on a private URL before publishing them
- Content in multiple languages using i18n
This starter uses the [Strapi corporate template](https://github.com/strapi/strapi-template-corporate)
Check out all of our starters [here](https://strapi.io/starters)
## Getting started
Use our `create-strapi-starter` CLI to create your project.
```sh
# Using Yarn
yarn create strapi-starter my-site next-corporate
# Or using NPM
npx create-strapi-starter my-site next-corporate
```
The CLI will create a monorepo, install dependencies, and run your project automatically.
The Next frontend server will run here => [http://localhost:3000](http://localhost:3000)
The Strapi backend server will run here => [http://localhost:1337](http://localhost:1337)
## Preview Mode
You can turn preview mode on with a URL like this:
`http://localhost:3000/api/preview?secret=&slug=`
`` is the secret token defined in your .env config,
`` is the slug you entered in Strapi for your page.
While preview mode is on you can access `draft` pages just like you would `published` pages.
For example [http://localhost:3000/secret](http://localhost:3000/secret) would be available in preview mode.
A banner will remain under the navigation to let you know preview mode is on and it will also allow you to turn it off.
## Customize your corporate site
To edit this website, you'll need to run both the frontend and the backend in your development environment.
### Adding Sections
We have built sections for you, but you will likely want to add more to fit your needs. Follow these steps:
- Create a new component in Strapi in the "sections" category
- In the Content-Types Builder, open the Pages collection and check your new section on the `contentSections` field.
- Create a React component that takes a `data` prop in `/frontend/components/sections`
- To link your Strapi section to this React component, open `/frontend/components/sections.js`, and add an entry to the `sectionComponents` object.
### Custom theme
We use Tailwind CSS for styling. To modify your page's look, you can edit the theme in `/front/tailwind.config.js`. Read the [Tailwind docs](https://v1.tailwindcss.com/docs/theme) to view all the changes you can make. For example, you can change the primary color like this:
```js
const { colors } = require(`tailwindcss/defaultTheme`);
module.exports = {
theme: {
extend: {
colors: {
primary: colors.green,
},
},
},
};
```
## Deploying to production
You will need to deploy the `frontend` and `backend` projects separately. Here are the docs to deploy each one:
- [Deploy Strapi](https://strapi.io/documentation/v3.x/admin-panel/deploy.html#deployment)
- [Deploy Next.js](https://nextjs.org/docs/deployment)
Don't forget to set up your environment variables on your production apps.
Here are the required ones for the frontend:
- `NEXT_PUBLIC_STRAPI_API_URL`: URL of your Strapi backend, without trailing slash
- `PREVIEW_SECRET`: a random string used to protect your preview pages
And for the backend:
- `FRONTEND_URL`: URL of your frontend, without trailing slash
- `FRONTEND_PREVIEW_SECRET`: token of Next.js preview mode defined on the frontend
Have fun using this starter!
================================================
FILE: packages/starters/next-corporate/package.json
================================================
{
"name": "@strapi/starter-next-corporate",
"version": "1.0.4",
"description": "Strapi corporate starter with Next.js",
"keywords": [
"strapi",
"starter",
"nextjs",
"corporate"
],
"homepage": "https://github.com/strapi/starters-and-templates/tree/main/packages/starters/next-corporate#readme",
"bugs": {
"url": "https://github.com/strapi/starters-and-templates/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/strapi/starters-and-templates.git"
},
"license": "MIT",
"author": {
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
},
"maintainers": [
{
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
}
]
}
================================================
FILE: packages/starters/next-corporate/starter/.eslintrc
================================================
{
"extends": [
"next",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
"printWidth": 80,
"singleQuote": false,
"trailingComma": "es5",
"semi": false,
"tabWidth": 2
}
]
}
}
================================================
FILE: packages/starters/next-corporate/starter/.gitignore
================================================
.vscode
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
================================================
FILE: packages/starters/next-corporate/starter/README.md
================================================
# Next frontend
This frontend relies on Next's [Static Generation](https://nextjs.org/docs/basic-features/pages) using [Strapi](https://strapi.io/) as the data source. Make sure Strapi is running in parallel when you run this app.
## Routes
**pages/[[...slug]].js**
This file generates all the app's route. First, it fetches all the pages entries in Strapi. Then, it creates one route per page found. These routes can look like this:
* yoursite.com
* yoursite.com/page
* yoursite.com/page/nested/route
Notice that the path of the page can be several layers deep, or it can be the root of the site. This is possible thanks to Next's [optional catch-all routes](https://nextjs.org/docs/routing/dynamic-routes#optional-catch-all-routes).
To see how to build these nested routes, see [the Strapi project's Readme](../backend/README.md).
## Available Scripts
In the project directory, you can run:
**`yarn dev`**
Runs the app in the development mode.
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.
You will also see any errors in the console.
**`yarn build`**
Builds the app for production to the `.next` folder.
It correctly bundles React in production mode and optimizes the build for the best performance.
**`yarn start`**
Starts the application in production mode.
The application should be compiled with \`next build\` first.
See the section in Next docs about [deployment](https://nextjs.org/docs/deployment) for more
information.
================================================
FILE: packages/starters/next-corporate/starter/components/elements/button-link.js
================================================
import classNames from "classnames"
import PropTypes from "prop-types"
import { buttonLinkPropTypes } from "utils/types"
import CustomLink from "./custom-link"
const ButtonContent = ({ button, appearance, compact }) => {
return (
{button.text}
)
}
const ButtonLink = ({ button, appearance, compact = false }) => {
return (
)
}
ButtonLink.propTypes = {
button: buttonLinkPropTypes,
appearance: PropTypes.oneOf([
"dark",
"white-outline",
"white",
"dark-outline",
]),
compact: PropTypes.bool,
}
export default ButtonLink
================================================
FILE: packages/starters/next-corporate/starter/components/elements/button.js
================================================
import classNames from "classnames"
import PropTypes from "prop-types"
import { buttonLinkPropTypes } from "utils/types"
import Loader from "./loader"
const Button = ({
button,
appearance,
compact = false,
handleClick,
loading = false,
type,
}) => {
return (
{loading && }
{button.text}
)
}
Button.propTypes = {
button: buttonLinkPropTypes,
appearance: PropTypes.oneOf([
"dark",
"white-outline",
"white",
"dark-outline",
]),
compact: PropTypes.bool,
}
export default Button
================================================
FILE: packages/starters/next-corporate/starter/components/elements/custom-link.js
================================================
import Link from "next/link"
import PropTypes from "prop-types"
import { linkPropTypes } from "utils/types"
const CustomLink = ({ link, children }) => {
const isInternalLink = link.url.startsWith("/")
// For internal links, use the Next.js Link component
if (isInternalLink) {
return (
{children}
)
}
// Plain tags for external links
if (link.newTab) {
return (
{children}
)
}
return (
{children}
)
}
CustomLink.propTypes = {
link: linkPropTypes,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
}
export default CustomLink
================================================
FILE: packages/starters/next-corporate/starter/components/elements/footer.js
================================================
import PropTypes from "prop-types"
import { linkPropTypes, mediaPropTypes } from "utils/types"
import NextImage from "./image"
import CustomLink from "./custom-link"
const Footer = ({ footer }) => {
return (
{footer.logo && (
)}
{footer.columns.map((footerColumn) => (
{footerColumn.title}
{footerColumn.links.map((link) => (
{link.text}
))}
))}
)
}
Footer.propTypes = {
footer: PropTypes.shape({
logo: mediaPropTypes.isRequired,
columns: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
title: PropTypes.string.isRequired,
links: PropTypes.arrayOf(linkPropTypes),
})
),
smallText: PropTypes.string.isRequired,
}),
}
export default Footer
================================================
FILE: packages/starters/next-corporate/starter/components/elements/image.js
================================================
import { getStrapiMedia } from "utils/media"
import Image from "next/image"
import PropTypes from "prop-types"
import { mediaPropTypes } from "utils/types"
const NextImage = ({ media, ...props }) => {
const { url, alternativeText, width, height } = media.data.attributes
const loader = ({ src, width }) => {
return getStrapiMedia(src)
}
// The image has a fixed width and height
if (props.width && props.height) {
return (
)
}
// The image is responsive
return (
)
}
Image.propTypes = {
media: mediaPropTypes.isRequired,
className: PropTypes.string,
}
export default NextImage
================================================
FILE: packages/starters/next-corporate/starter/components/elements/loader.js
================================================
import React from "react"
const Loader = () => {
return (
)
}
export default Loader
================================================
FILE: packages/starters/next-corporate/starter/components/elements/mobile-nav-menu.js
================================================
import PropTypes from "prop-types"
import { MdClose, MdChevronRight } from "react-icons/md"
import { mediaPropTypes, linkPropTypes, buttonLinkPropTypes } from "utils/types"
import { useLockBodyScroll } from "utils/hooks"
import { getButtonAppearance } from "utils/button"
import ButtonLink from "./button-link"
import NextImage from "./image"
import CustomLink from "./custom-link"
const MobileNavMenu = ({ navbar, closeSelf }) => {
// Prevent window scroll while mobile nav menu is open
useLockBodyScroll()
return (
{/* Top section */}
{/* Company logo */}
{/* Close button */}
{/* Bottom section */}
{navbar.links.map((navLink) => (
{navLink.text}
))}
)
}
MobileNavMenu.propTypes = {
navbar: PropTypes.shape({
logo: mediaPropTypes,
links: PropTypes.arrayOf(linkPropTypes),
button: buttonLinkPropTypes,
}),
closeSelf: PropTypes.func,
}
export default MobileNavMenu
================================================
FILE: packages/starters/next-corporate/starter/components/elements/navbar.js
================================================
import { useState } from "react"
import PropTypes from "prop-types"
import Link from "next/link"
import { useRouter } from "next/router"
import { getButtonAppearance } from "utils/button"
import { mediaPropTypes, linkPropTypes, buttonLinkPropTypes } from "utils/types"
import { MdMenu } from "react-icons/md"
import MobileNavMenu from "./mobile-nav-menu"
import ButtonLink from "./button-link"
import NextImage from "./image"
import CustomLink from "./custom-link"
import LocaleSwitch from "../locale-switch"
const Navbar = ({ navbar, pageContext }) => {
const router = useRouter()
const [mobileMenuIsShown, setMobileMenuIsShown] = useState(false)
return (
<>
{/* The actual navbar */}
{/* Content aligned to the left */}
{/* List of links on desktop */}
{navbar.links.map((navLink) => (
{navLink.text}
))}
{/* Locale Switch Mobile */}
{pageContext.localizedPaths && (
)}
{/* Hamburger menu on mobile */}
setMobileMenuIsShown(true)}
className="p-1 block md:hidden"
>
{/* CTA button on desktop */}
{navbar.button && (
)}
{/* Locale Switch Desktop */}
{pageContext.localizedPaths && (
)}
{/* Mobile navigation menu panel */}
{mobileMenuIsShown && (
setMobileMenuIsShown(false)}
/>
)}
>
)
}
Navbar.propTypes = {
navbar: PropTypes.shape({
logo: PropTypes.shape({
image: mediaPropTypes,
url: PropTypes.string,
}),
links: PropTypes.arrayOf(linkPropTypes),
button: buttonLinkPropTypes,
}),
initialLocale: PropTypes.string,
}
export default Navbar
================================================
FILE: packages/starters/next-corporate/starter/components/elements/notification-banner.js
================================================
import Markdown from "react-markdown"
import classNames from "classnames"
import { MdClose } from "react-icons/md"
const NotificationBanner = ({ data: { text, type }, closeSelf }) => {
return (
)
}
export default NotificationBanner
================================================
FILE: packages/starters/next-corporate/starter/components/elements/seo.js
================================================
import { NextSeo } from "next-seo"
import PropTypes from "prop-types"
import { getStrapiMedia } from "utils/media"
import { mediaPropTypes } from "utils/types"
const Seo = ({ metadata }) => {
// Prevent errors if no metadata was set
if (!metadata) return null
return (
{
return {
url: getStrapiMedia(image.url),
width: image.width,
height: image.height,
}
}),
}),
}}
// Only included Twitter data if we have it
twitter={{
...(metadata.twitterCardType && { cardType: metadata.twitterCardType }),
// Handle is the twitter username of the content creator
...(metadata.twitterUsername && { handle: metadata.twitterUsername }),
}}
/>
)
}
Seo.propTypes = {
metadata: PropTypes.shape({
metaTitle: PropTypes.string.isRequired,
metaDescription: PropTypes.string.isRequired,
shareImage: mediaPropTypes,
twitterCardType: PropTypes.string,
twitterUsername: PropTypes.string,
}),
}
export default Seo
================================================
FILE: packages/starters/next-corporate/starter/components/elements/video.js
================================================
import PropTypes from "prop-types"
import { getStrapiMedia } from "utils/media"
import { mediaPropTypes } from "utils/types"
const Video = ({
media,
poster,
className,
controls = true,
autoPlay = false,
}) => {
const fullVideoUrl = getStrapiMedia(media.url)
const fullPosterUrl = getStrapiMedia(poster?.url)
return (
)
}
Video.propTypes = {
media: mediaPropTypes.isRequired,
poster: mediaPropTypes,
className: PropTypes.string,
controls: PropTypes.bool,
autoPlay: PropTypes.bool,
}
export default Video
================================================
FILE: packages/starters/next-corporate/starter/components/icons/world.js
================================================
import React from "react"
const WorldIcon = () => {
return (
)
}
export default WorldIcon
================================================
FILE: packages/starters/next-corporate/starter/components/layout.js
================================================
import { useState } from "react"
import Navbar from "./elements/navbar"
import Footer from "./elements/footer"
import NotificationBanner from "./elements/notification-banner"
const Layout = ({ children, global, pageContext }) => {
const { navbar, footer, notificationBanner } = global.attributes
const [bannerIsShown, setBannerIsShown] = useState(true)
return (
{/* Aligned to the top */}
{notificationBanner && bannerIsShown && (
setBannerIsShown(false)}
/>
)}
{children}
{/* Aligned to the bottom */}
)
}
export default Layout
================================================
FILE: packages/starters/next-corporate/starter/components/locale-switch.js
================================================
import { useEffect, useState, useRef } from "react"
import { useRouter } from "next/router"
import PropTypes from "prop-types"
import Link from "next/link"
import Cookies from "js-cookie"
import { MdExpandMore } from "react-icons/md"
import WorldIcon from "./icons/world"
import { useOnClickOutside } from "../utils/hooks"
const LocaleSwitch = ({ pageContext }) => {
const isMounted = useRef(false)
const select = useRef()
const router = useRouter()
const [locale, setLocale] = useState()
const [showing, setShowing] = useState(false)
const handleLocaleChange = async (selectedLocale) => {
// Persist the user's language preference
// https://nextjs.org/docs/advanced-features/i18n-routing#leveraging-the-next_locale-cookie
Cookies.set("NEXT_LOCALE", selectedLocale)
setLocale(selectedLocale)
}
const handleLocaleChangeRef = useRef(handleLocaleChange)
useOnClickOutside(select, () => setShowing(false))
useEffect(() => {
const localeCookie = Cookies.get("NEXT_LOCALE")
if (!localeCookie) {
handleLocaleChangeRef.current(router.locale)
}
const checkLocaleMismatch = async () => {
if (
!isMounted.current &&
localeCookie &&
localeCookie !== pageContext.locale
) {
// Redirect to locale page if locale mismatch
const localePage = getLocalizedPage(localeCookie, pageContext)
router.push(
`${localizePath({ ...pageContext, ...localePage })}`,
`${localizePath({ ...pageContext, ...localePage })}`,
{ locale: localePage.locale }
)
}
setShowing(false)
}
setLocale(localeCookie || router.locale)
checkLocaleMismatch()
return () => {
isMounted.current = true
}
}, [locale, router, pageContext])
return (
setShowing(!showing)}
>
{locale}
{pageContext.localizedPaths &&
pageContext.localizedPaths.map(({ href, locale }) => {
return (
handleLocaleChange(locale)}
className="capitalize hover:bg-primary-50 hover:text-primary-600 cursor-pointer p-2 rounded-md text-center hover:text-primary-600"
>
{locale}
)
})}
)
}
LocaleSwitch.propTypes = {
initialLocale: PropTypes.string,
}
export default LocaleSwitch
================================================
FILE: packages/starters/next-corporate/starter/components/sections/bottom-actions.js
================================================
import ButtonLink from "@/components/elements/button-link"
import { getButtonAppearance } from "utils/button"
const BottomActions = ({ data }) => {
return (
{data.title}
{/* Buttons row */}
{data.buttons.map((button) => (
))}
)
}
export default BottomActions
================================================
FILE: packages/starters/next-corporate/starter/components/sections/feature-columns-group.js
================================================
import NextImage from "../elements/image"
const FeatureColumnsGroup = ({ data }) => {
return (
{data.features.map((feature) => (
{feature.title}
{feature.description}
))}
)
}
export default FeatureColumnsGroup
================================================
FILE: packages/starters/next-corporate/starter/components/sections/feature-rows-group.js
================================================
import classNames from "classnames"
import NextImage from "../elements/image"
import Video from "../elements/video"
import CustomLink from "../elements/custom-link"
const FeatureRowsGroup = ({ data }) => {
return (
{data.features.map((feature, index) => (
{/* Text section */}
{feature.title}
{feature.description}
{feature.link.text}
{/* Media section */}
{/* Images */}
{feature.media.data.attributes.mime.startsWith("image") && (
)}
{/* Videos */}
{feature.media.data.attributes.mime.startsWith("video") && (
)}
))}
)
}
export default FeatureRowsGroup
================================================
FILE: packages/starters/next-corporate/starter/components/sections/hero.js
================================================
import Markdown from "react-markdown"
import { getButtonAppearance } from "utils/button"
import ButtonLink from "../elements/button-link"
import NextImage from "../elements/image"
const Hero = ({ data }) => {
return (
{/* Left column for content */}
{/* Hero section label */}
{data.label}
{/* Big title */}
{data.title}
{/* Description paragraph */}
{data.description}
{/* Buttons row */}
{data.buttons.map((button) => (
))}
{/* Small rich text */}
{data.smallTextWithLink}
{/* Right column for the image */}
)
}
export default Hero
================================================
FILE: packages/starters/next-corporate/starter/components/sections/large-video.js
================================================
import Video from "../elements/video"
const LargeVideo = ({ data }) => {
return (
{data.title}
{data.description}
{/* Video wrapper */}
)
}
export default LargeVideo
================================================
FILE: packages/starters/next-corporate/starter/components/sections/lead-form.js
================================================
import { useState } from "react"
import { fetchAPI } from "utils/api"
import * as yup from "yup"
import { Formik, Form, Field } from "formik"
import Button from "../elements/button"
const LeadForm = ({ data }) => {
const [loading, setLoading] = useState(false)
const LeadSchema = yup.object().shape({
email: yup.string().email().required(),
})
return (
{data.title}
{
setLoading(true)
try {
setErrors({ api: null })
await fetchAPI(
"/lead-form-submissions",
{},
{
method: "POST",
body: JSON.stringify({
data: {
email: values.email,
location: data.location,
},
}),
}
)
} catch (err) {
setErrors({ api: err.message })
}
setLoading(false)
setSubmitting(false)
}}
>
{({ errors, touched, isSubmitting }) => (
{(errors.email && touched.email && errors.email) || errors.api}
)}
)
}
export default LeadForm
================================================
FILE: packages/starters/next-corporate/starter/components/sections/pricing.js
================================================
import { MdCheckBox } from "react-icons/md"
import classNames from "classnames"
const Pricing = ({ data }) => {
return (
{data.title}
{data.plans.map((plan) => (
{plan.name}
{plan.description}
{plan.price === 0 ? "Free " : `$${plan.price} `}
{plan.pricePeriod}
{plan.features.map((feature) => (
{feature.name}
))}
))}
)
}
export default Pricing
================================================
FILE: packages/starters/next-corporate/starter/components/sections/rich-text.js
================================================
import PropTypes from "prop-types"
import Markdown from "react-markdown"
const RichText = ({ data }) => {
return (
{data.content}
)
}
RichText.propTypes = {
data: PropTypes.shape({
content: PropTypes.string,
}),
}
export default RichText
================================================
FILE: packages/starters/next-corporate/starter/components/sections/testimonials-group.js
================================================
import classNames from "classnames"
import { useState } from "react"
import NextImage from "../elements/image"
import CustomLink from "../elements/custom-link"
const TestimonialsGroup = ({ data }) => {
// Only show one testimonial at a time
const [selectedTestimonialIndex, setSelectedTestimonialIndex] = useState(0)
const selectedTestimonial = data.testimonials[selectedTestimonialIndex]
return (
{data.title}
{data.description}
{data.link.text}
{/* Current testimonial card */}
"{selectedTestimonial.text}"
{selectedTestimonial.authorName}
{selectedTestimonial.authorTitle}
Read story
{/* Change selected testimonial (only if there is more than one) */}
{data.testimonials.length > 1 && (
{data.testimonials.map((testimonial, index) => (
setSelectedTestimonialIndex(index)}
className={classNames(
// Common classes
"rounded-full h-3 w-3",
{
"bg-gray-500": index !== selectedTestimonialIndex,
"bg-primary-600": index === selectedTestimonialIndex,
}
)}
key={testimonial.id}
/>
))}
)}
{/* Logos list */}
{data.logos.map((logo) => (
))}
)
}
export default TestimonialsGroup
================================================
FILE: packages/starters/next-corporate/starter/components/sections.js
================================================
import { useRouter } from "next/router"
import Hero from "@/components/sections/hero"
import LargeVideo from "@/components/sections/large-video"
import FeatureColumnsGroup from "@/components/sections/feature-columns-group"
import FeatureRowsGroup from "@/components/sections/feature-rows-group"
import BottomActions from "@/components/sections/bottom-actions"
import TestimonialsGroup from "@/components/sections/testimonials-group"
import RichText from "./sections/rich-text"
import Pricing from "./sections/pricing"
import LeadForm from "./sections/lead-form"
// Map Strapi sections to section components
const sectionComponents = {
ComponentSectionsHero: Hero,
ComponentSectionsLargeVideo: LargeVideo,
ComponentSectionsFeatureColumnsGroup: FeatureColumnsGroup,
ComponentSectionsFeatureRowsGroup: FeatureRowsGroup,
ComponentSectionsBottomActions: BottomActions,
ComponentSectionsTestimonialsGroup: TestimonialsGroup,
ComponentSectionsRichText: RichText,
ComponentSectionsPricing: Pricing,
ComponentSectionsLeadForm: LeadForm,
}
// Display a section individually
const Section = ({ sectionData }) => {
// Prepare the component
const SectionComponent = sectionComponents[sectionData.__typename]
if (!SectionComponent) {
return null
}
// Display the section
return
}
const PreviewModeBanner = () => {
const router = useRouter()
const exitURL = `/api/exit-preview?redirect=${encodeURIComponent(
router.asPath
)}`
return (
)
}
// Display the list of sections
const Sections = ({ sections, preview }) => {
return (
{/* Show a banner if preview mode is on */}
{preview &&
}
{/* Show the actual sections */}
{sections.map((section) => (
))}
)
}
export default Sections
================================================
FILE: packages/starters/next-corporate/starter/jsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": [
"components/*"
],
"@/lib/*": [
"lib/*"
],
"@/styles/*": [
"styles/*"
]
}
}
}
================================================
FILE: packages/starters/next-corporate/starter/next.config.js
================================================
module.exports = {
i18n: {
locales: ['en', 'fr'],
defaultLocale: 'en',
},
}
================================================
FILE: packages/starters/next-corporate/starter/package.json
================================================
{
"name": "my-next-corporate",
"version": "1.0.7",
"private": true,
"scripts": {
"dev": "next",
"develop": "next",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix"
},
"dependencies": {
"@tailwindcss/typography": "^0.4.1",
"classnames": "^2.2.6",
"cookie": "^0.4.1",
"date-fns": "2.14.0",
"formik": "^2.2.6",
"js-cookie": "3.0.0-rc.4",
"next": "^11.0.0",
"next-seo": "^4.7.1",
"postcss-import": "^14.0.2",
"prop-types": "^15.7.2",
"qs": "^6.10.1",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-icons": "^3.10.0",
"react-markdown": "^6.0.2",
"tailwindcss": "^2.2.6",
"yup": "^0.32.8"
},
"devDependencies": {
"autoprefixer": "^10.3.1",
"eslint": "^7.30.0",
"eslint-config-next": "^11.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"postcss": "8.3.6",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-nested": "^5.0.5",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.3.1"
}
}
================================================
FILE: packages/starters/next-corporate/starter/pages/[[...slug]].js
================================================
import ErrorPage from "next/error"
import { getPageData, fetchAPI, getGlobalData } from "utils/api"
import Sections from "@/components/sections"
import Seo from "@/components/elements/seo"
import { useRouter } from "next/router"
import Layout from "@/components/layout"
import { getLocalizedPaths } from "utils/localize"
// The file is called [[...slug]].js because we're using Next's
// optional catch all routes feature. See the related docs:
// https://nextjs.org/docs/routing/dynamic-routes#optional-catch-all-routes
const DynamicPage = ({ sections, metadata, preview, global, pageContext }) => {
const router = useRouter()
// Check if the required data was provided
if (!router.isFallback && !sections?.length) {
return
}
// Loading screen (only possible in preview mode)
if (router.isFallback) {
return Loading...
}
// Merge default site SEO settings with page specific SEO settings
if (metadata.shareImage?.data == null) {
delete metadata.shareImage
}
const metadataWithDefaults = {
...global.attributes.metadata,
...metadata,
}
return (
{/* Add meta tags for SEO*/}
{/* Display content sections */}
)
}
export async function getStaticPaths(context) {
// Get all pages from Strapi
const pages = await context.locales.reduce(
async (currentPagesPromise, locale) => {
const currentPages = await currentPagesPromise
const localePages = await fetchAPI("/pages", {
locale,
fields: ["slug", "locale"],
})
return [...currentPages, ...localePages.data]
},
Promise.resolve([])
)
const paths = pages.map((page) => {
const { slug, locale } = page.attributes
// Decompose the slug that was saved in Strapi
const slugArray = !slug ? false : slug.split("/")
return {
params: { slug: slugArray },
// Specify the locale to render
locale,
}
})
return { paths, fallback: true }
}
export async function getStaticProps(context) {
const { params, locale, locales, defaultLocale, preview = null } = context
const globalLocale = await getGlobalData(locale)
// Fetch pages. Include drafts if preview mode is on
const pageData = await getPageData({
slug: (!params.slug ? [""] : params.slug).join("/"),
locale,
preview,
})
if (pageData == null) {
// Giving the page no props will trigger a 404 page
return { props: {} }
}
// We have the required page data, pass it to the page component
const { contentSections, metadata, localizations, slug } = pageData.attributes
const pageContext = {
locale,
locales,
defaultLocale,
slug,
localizations,
}
const localizedPaths = getLocalizedPaths(pageContext)
return {
props: {
preview,
sections: contentSections,
metadata,
global: globalLocale.data,
pageContext: {
...pageContext,
localizedPaths,
},
},
}
}
export default DynamicPage
================================================
FILE: packages/starters/next-corporate/starter/pages/_app.js
================================================
import App from "next/app"
import Head from "next/head"
import ErrorPage from "next/error"
import { useRouter } from "next/router"
import { DefaultSeo } from "next-seo"
import { getStrapiMedia } from "utils/media"
import { getGlobalData } from "utils/api"
import "@/styles/index.css"
const MyApp = ({ Component, pageProps }) => {
// Extract the data we need
const { global } = pageProps
if (global == null) {
return
}
const { metadata, favicon, metaTitleSuffix } = global.attributes
return (
<>
{/* Favicon */}
{/* Global site metadata */}
{
return {
url: getStrapiMedia(image.url),
width: image.width,
height: image.height,
}
}),
}}
twitter={{
cardType: metadata.twitterCardType,
handle: metadata.twitterUsername,
}}
/>
{/* Display the content */}
>
)
}
// getInitialProps disables automatic static optimization for pages that don't
// have getStaticProps. So [[...slug]] pages still get SSG.
// Hopefully we can replace this with getStaticProps once this issue is fixed:
// https://github.com/vercel/next.js/discussions/10949
MyApp.getInitialProps = async (appContext) => {
// Calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(appContext)
const globalLocale = await getGlobalData(appContext.router.locale)
return {
...appProps,
pageProps: {
global: globalLocale,
},
}
}
export default MyApp
================================================
FILE: packages/starters/next-corporate/starter/pages/_document.js
================================================
import Document, { Html, Head, Main, NextScript } from "next/document"
export default class MyDocument extends Document {
render() {
return (
)
}
}
================================================
FILE: packages/starters/next-corporate/starter/pages/api/exit-preview.js
================================================
import { redirect } from "next/dist/next-server/server/api-utils"
export default async function exit(req, res) {
// Exit the current user from "Preview Mode". This function accepts no args.
res.clearPreviewData()
// Redirect the user back to a provided redirect path or the index page
res.writeHead(307, { Location: "/" })
res.end()
}
================================================
FILE: packages/starters/next-corporate/starter/pages/api/preview.js
================================================
import { getPageData } from "utils/api"
import { parseCookies } from "utils/parse-cookies"
const preview = async (req, res) => {
// Check the secret and next parameters
// This secret should only be known to this API route and the CMS
if (req.query.secret !== (process.env.PREVIEW_SECRET || "secret-token")) {
return res.status(401).json({ message: "Invalid token" })
}
const cookies = parseCookies(req)
const slugArray = req.query.slug.split("/")
// Fetch the headless CMS to check if the provided `slug` exists
const pageData = await getPageData({
locale,
slug: slugArray.join("/"),
preview: true,
})
// If the slug doesn't exist prevent preview mode from being enabled
if (!pageData) {
return res.status(401).json({ message: "Invalid slug" })
}
// Enable Preview Mode by setting the cookies
res.setPreviewData({})
// Redirect to the path from the fetched post
// We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
// Prefix with locale so previews are available in all languages
res.writeHead(307, {
Location: `/${pageData.locale}/${pageData.slug}`,
})
res.end()
}
export default preview
// You can view Preview pages with URLs like this:
// http://localhost:3000/api/preview?secret=&slug=
// where is the secret token defined in your .env config
// and where is the slug you entered in Strapi for your page
// The slug must match the current locale
================================================
FILE: packages/starters/next-corporate/starter/postcss.config.js
================================================
module.exports = {
plugins: [
"postcss-import",
"tailwindcss",
"postcss-flexbugs-fixes",
"postcss-nesting",
"postcss-custom-properties",
"autoprefixer",
],
}
================================================
FILE: packages/starters/next-corporate/starter/public/.gitkeep
================================================
================================================
FILE: packages/starters/next-corporate/starter/styles/index.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
font-size: 13px;
@apply text-gray-900;
}
@media screen and (min-width: 640px) {
html {
font-size: 14px;
}
}
@media screen and (min-width: 768px) {
html {
font-size: 15px;
}
}
.rich-text-hero a {
@apply text-blue-600 underline;
}
.rich-text-banner {
@apply whitespace-pre-line;
a {
@apply underline;
}
}
.title {
@apply text-4xl leading-snug font-semibold;
}
@media (min-width: 768px) {
.title {
@apply text-5xl;
}
}
.with-arrow:after {
background-image: url(data:image/svg+xml;charset=utf-8,%3Csvg%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M1%206a.5.5%200%200%200%200%201V6zM12.854.646a.5.5%200%200%200-.708.708l.708-.708zM18%206.5l.354.354a.5.5%200%200%200%200-.708L18%206.5zm-5.854%205.146a.5.5%200%200%200%20.708.708l-.708-.708zM1%207h16.5V6H1v1zm16.646-.854l-5.5%205.5.708.708%205.5-5.5-.708-.708zm-5.5-4.792l2.75%202.75.708-.708-2.75-2.75-.708.708zm2.75%202.75l2.75%202.75.708-.708-2.75-2.75-.708.708z%22%20fill%3D%22%231264A3%22%2F%3E%3C%2Fsvg%3E);
content: "";
width: 19px;
height: 13px;
display: inline-block;
margin-left: 0.5em;
}
================================================
FILE: packages/starters/next-corporate/starter/tailwind.config.js
================================================
const { colors } = require(`tailwindcss/defaultTheme`)
module.exports = {
mode: "jit", // see https://tailwindcss.com/docs/just-in-time-mode
purge: ["./components/**/*.js", "./pages/**/*.js"],
darkMode: false, // or "media" or "class"
theme: {
extend: {
colors: {
primary: colors.indigo,
},
container: {
center: true,
padding: {
DEFAULT: "1rem",
md: "2rem",
},
},
},
screens: {
sm: "640px",
md: "768px",
lg: "1024px",
xl: "1280px",
},
},
variants: {
extend: {},
},
plugins: [require("@tailwindcss/typography")],
}
================================================
FILE: packages/starters/next-corporate/starter/utils/api.js
================================================
import qs from "qs"
export function getStrapiURL(path) {
return `${
process.env.NEXT_PUBLIC_STRAPI_API_URL || "http://localhost:1337"
}${path}`
}
/**
* Helper to make GET requests to Strapi API endpoints
* @param {string} path Path of the API route
* @param {Object} urlParamsObject URL params object, will be stringified
* @param {RequestInit} options Options passed to fetch
* @returns Parsed API call response
*/
export async function fetchAPI(path, urlParamsObject = {}, options = {}) {
// Merge default and user options
const mergedOptions = {
headers: {
"Content-Type": "application/json",
},
...options,
}
// Build request URL
const queryString = qs.stringify(urlParamsObject)
const requestUrl = `${getStrapiURL(
`/api${path}${queryString ? `?${queryString}` : ""}`
)}`
// Trigger API call
const response = await fetch(requestUrl, mergedOptions)
// Handle response
if (!response.ok) {
console.error(response.statusText)
throw new Error(`An error occurred please try again`)
}
const data = await response.json()
return data
}
/**
*
* @param {Object} options
* @param {string} options.slug The page's slug
* @param {string} options.locale The current locale specified in router.locale
* @param {boolean} options.preview router isPreview value
*/
export async function getPageData({ slug, locale, preview }) {
// Find the pages that match this slug
const gqlEndpoint = getStrapiURL("/graphql")
const pagesRes = await fetch(gqlEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
fragment FileParts on UploadFileEntityResponse {
data {
id
attributes {
alternativeText
width
height
mime
url
formats
}
}
}
query GetPages(
$slug: String!
$publicationState: PublicationState!
$locale: I18NLocaleCode!
) {
pages(
filters: { slug: { eq: $slug } }
publicationState: $publicationState
locale: $locale
) {
data {
id
attributes {
locale
localizations {
data {
id
attributes {
locale
}
}
}
slug
metadata {
metaTitle
metaDescription
shareImage {
...FileParts
}
twitterCardType
twitterUsername
}
contentSections {
__typename
... on ComponentSectionsBottomActions {
id
title
buttons {
id
newTab
text
type
url
}
}
... on ComponentSectionsHero {
id
buttons {
id
newTab
text
type
url
}
title
description
label
picture {
...FileParts
}
}
... on ComponentSectionsFeatureColumnsGroup {
id
features {
id
description
icon {
...FileParts
}
title
}
}
... on ComponentSectionsFeatureRowsGroup {
id
features {
id
description
link {
id
newTab
text
url
}
media {
...FileParts
}
title
}
}
... on ComponentSectionsTestimonialsGroup {
id
description
link {
id
newTab
text
url
}
logos {
id
title
logo {
...FileParts
}
}
testimonials {
id
logo {
...FileParts
}
picture {
...FileParts
}
text
authorName
authorTitle
link
}
title
}
... on ComponentSectionsLargeVideo {
id
description
title
poster {
...FileParts
}
video {
...FileParts
}
}
... on ComponentSectionsRichText {
id
content
}
... on ComponentSectionsPricing {
id
title
plans {
description
features {
id
name
}
id
isRecommended
name
price
pricePeriod
}
}
... on ComponentSectionsLeadForm {
id
emailPlaceholder
location
submitButton {
id
text
type
}
title
}
}
}
}
}
}
`,
variables: {
slug,
publicationState: preview ? "PREVIEW" : "LIVE",
locale,
},
}),
})
const pagesData = await pagesRes.json()
// Make sure we found something, otherwise return null
if (pagesData.data?.pages == null || pagesData.data.pages.length === 0) {
return null
}
// Return the first item since there should only be one result per slug
return pagesData.data.pages.data[0]
}
// Get site data from Strapi (metadata, navbar, footer...)
export async function getGlobalData(locale) {
const gqlEndpoint = getStrapiURL("/graphql")
const globalRes = await fetch(gqlEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
fragment FileParts on UploadFileEntityResponse {
data {
id
attributes {
alternativeText
width
height
mime
url
formats
}
}
}
query GetGlobal($locale: I18NLocaleCode!) {
global(locale: $locale) {
data {
id
attributes {
favicon {
...FileParts
}
metadata {
metaTitle
metaDescription
shareImage {
...FileParts
}
twitterCardType
twitterUsername
}
metaTitleSuffix
notificationBanner {
type
text
}
navbar {
logo {
...FileParts
}
links {
id
url
newTab
text
}
button {
id
url
newTab
text
type
}
}
footer {
logo {
...FileParts
}
smallText
columns {
id
title
links {
id
url
newTab
text
}
}
}
}
}
}
}
`,
variables: {
locale,
},
}),
})
const global = await globalRes.json()
return global.data.global
}
================================================
FILE: packages/starters/next-corporate/starter/utils/button.js
================================================
// Decide what the button will look like based on its type (primary or secondary)
// and on its background (light or dark).
export function getButtonAppearance(type, background) {
if (type === 'primary') {
if (background === 'light') {
// Dark primary button on a light background
return 'dark'
}
// Fully white primary button on a dark background
return 'white'
}
if (type === 'secondary') {
if (background === 'light') {
// Dark outline primary button on a light background
return 'dark-outline'
}
// White outline primary button on a dark background
return 'white-outline'
}
// Shouldn't happen, but default to dark button just in case
return 'dark'
}
================================================
FILE: packages/starters/next-corporate/starter/utils/hooks.js
================================================
import { useEffect } from 'react'
// Got from https://usehooks.com/useLockBodyScroll/
export function useLockBodyScroll() {
useEffect(() => {
// Get original body overflow
const originalStyle = window.getComputedStyle(document.body).overflow
// Prevent scrolling on mount
document.body.style.overflow = 'hidden'
// Re-enable scrolling when component unmounts
return () => (document.body.style.overflow = originalStyle)
}, []) // Empty array ensures effect is only run on mount and unmount
}
export function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target)) {
return
}
handler(event)
}
document.addEventListener('mousedown', listener)
document.addEventListener('touchstart', listener)
return () => {
document.removeEventListener('mousedown', listener)
document.removeEventListener('touchstart', listener)
}
}, [ref, handler])
}
================================================
FILE: packages/starters/next-corporate/starter/utils/localize.js
================================================
import { fetchAPI } from "./api"
export async function getLocalizedPage(targetLocale, pageContext) {
const localization = pageContext.localizations.data.find(
(localization) => localization.attributes.locale === targetLocale
)
const localePage = await fetchAPI(`/pages/${localization.id}`)
return localePage
}
export function localizePath(page) {
const { locale, defaultLocale, slug } = page
if (locale === defaultLocale) {
// The default locale is not prefixed
return `/${slug}`
}
// The slug should have a localePrefix
return `/${locale}/${slug}`
}
export function getLocalizedPaths(page) {
const paths = page.locales.map((locale) => {
return {
locale: locale,
href: localizePath({ ...page, locale }),
}
})
return paths
}
================================================
FILE: packages/starters/next-corporate/starter/utils/media.js
================================================
export function getStrapiMedia(url) {
if (url == null) {
return null
}
// Return the full URL if the media is hosted on an external provider
if (url.startsWith("http") || url.startsWith("//")) {
return url
}
// Otherwise prepend the URL path with the Strapi URL
return `${
process.env.NEXT_PUBLIC_STRAPI_API_URL || "http://localhost:1337"
}${url}`
}
================================================
FILE: packages/starters/next-corporate/starter/utils/parse-cookies.js
================================================
import cookie from "cookie"
export function parseCookies(req) {
return cookie.parse(req ? req.headers.cookie || '' : document.cookie)
}
================================================
FILE: packages/starters/next-corporate/starter/utils/types.js
================================================
import PropTypes from "prop-types"
export const linkPropTypes = PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
url: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
newTab: PropTypes.bool,
})
export const mediaPropTypes = PropTypes.shape({
data: PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
attributes: PropTypes.shape({
alternativeText: PropTypes.string,
mime: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
}),
}),
})
export const buttonLinkPropTypes = PropTypes.shape({
theme: PropTypes.string,
text: PropTypes.string.isRequired,
newTab: PropTypes.bool,
})
================================================
FILE: packages/starters/next-corporate/starter.json
================================================
{
"template": {
"name": "@strapi/template-corporate",
"version": "^1.0.0"
}
}
================================================
FILE: packages/templates/.gitkeep
================================================
================================================
FILE: packages/templates/blog/README.md
================================================
# strapi-template-blog
A Strapi template to create Strapi projects pre-configured for blog apps.
## Usage
```bash
# Using Yarn
yarn create strapi-app my-app-name --template blog
# Or using NPM
npx create-strapi-app my-app-name --template blog
```
## Starters
This template is used by the following starters:
* [strapi-starter-next-blog](https://github.com/strapi/strapi-starter-next-blog)
* [strapi-starter-nuxt-blog](https://github.com/strapi/strapi-starter-nuxt-blog)
* [strapi-starter-gatsby-blog](https://github.com/strapi/strapi-starter-gatsby-blog)
* [strapi-starter-gridsome-blog](https://github.com/strapi/strapi-starter-gridsome-blog)
================================================
FILE: packages/templates/blog/package.json
================================================
{
"name": "@strapi/template-blog",
"version": "2.0.5",
"description": "Strapi blog template",
"keywords": [
"strapi",
"template",
"blog"
],
"homepage": "https://github.com/strapi/starters-and-templates#readme",
"bugs": {
"url": "https://github.com/strapi/starters-and-templates/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/strapi/starters-and-templates.git"
},
"license": "MIT",
"author": {
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
},
"maintainers": [
{
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
}
],
"gitHead": "d6d5bd264d1df0a0f23b5234d84348f8d78882e3"
}
================================================
FILE: packages/templates/blog/template/data/data.json
================================================
{
"global": {
"siteName": "Strapi Blog",
"defaultSeo": {
"metaTitle": "Page",
"metaDescription": "A blog made with Strapi",
"shareImage": null
},
"siteDescription": "A Blog made with Strapi",
"favicon": null
},
"about": {
"title": "About the strapi blog",
"blocks": [
{
"__component": "shared.quote",
"title": "Thelonius Monk",
"body": "You've got to dig it to dig it, you dig?"
},
{
"__component": "shared.rich-text",
"body": "## Dedit imago conspicuus cum capillis totidem inhibere\n\nLorem markdownum **rerum**, est limine: columbas: ab infelix hostem arbore nudis\ncrudelis. Videtur reliquit ambo ferrum dote sub amne fatis **illuc**, in magis,\nnec."
},
{
"__component": "shared.media",
"file": "coffee-art.jpg"
}
]
},
"categories": [
{
"name": "news",
"slug": "news"
},
{
"name": "tech",
"slug": "tech"
},
{
"name": "food",
"slug": "food"
},
{
"name": "nature",
"slug": "nature"
},
{
"name": "story",
"slug": "story"
}
],
"authors": [
{
"name": "David Doe",
"email": "daviddoe@strapi.io",
"avatar": "daviddoe@strapi.io.jpg"
},
{
"name": "Sarah Baker",
"email": "sarahbaker@strapi.io",
"avatar": "sarahbaker@strapi.io.jpg"
}
],
"articles": [
{
"title": "The internet's Own boy",
"slug": "the-internet-s-own-boy",
"category": {
"id": 5
},
"author": {
"id": 1
},
"description": "Follow the story of Aaron Swartz, the boy who could change the world",
"cover": null,
"blocks": [
{
"__component": "shared.rich-text",
"body": "## Probant \n\nse Lorem markdownum negat. Argo *saxa* videnda cornuaque hunc qui tanta spes teneas! Obliquis est dicenti est salutat ille tamen iuvenum nostrae dolore. - Colores nocituraque comitata eripiunt - Addit quodcunque solum cui et dextram illis - Nulli meus nec extemplo ille ferebat pressit Se blandita fulvae vox gravem Pittheus cesserunt sanguine herbis tu comitum tenuit. Sui in ruunt; Doridaque maculosae fuissem! Et loqui. \n\n## Abit sua\n\nse Lorem markdownum negat. Argo *saxa* videnda cornuaque hunc qui tanta spes teneas! Obliquis est dicenti est salutat ille tamen iuvenum nostrae dolore. - Colores nocituraque comitata eripiunt - Addit quodcunque solum cui et dextram illis - Nulli meus nec extemplo ille ferebat pressit Se blandita fulvae vox gravem Pittheus cesserunt sanguine herbis tu comitum tenuit. Sui in ruunt; Doridaque maculosae fuissem! Et loqui. "
},
{
"__component": "shared.quote",
"title": "Thelonius Monk",
"body": "You've got to dig it to dig it, you dig?"
},
{
"__component": "shared.media",
"file": "coffee-art.jpg"
},
{
"__component": "shared.rich-text",
"body": "## Spatiantia astra \n\nFoeda, medio silva *errandum*: onus formam munere. Mutata bibulis est auxiliare arces etiamnunc verbis virgineo Priamidas illa Thescelus, nam fit locis lucis auras. Exitus hospes gratulor ut pondere [speslimite](http://www.curas.io/figuram); quid habent, Avernales faciente de. Pervenit Ino sonabile supplex cognoscenti vires, Bacchumque errat miserarum venandi dignabere dedisti. Discrimina iuncosaque virgaque tot sine superest [fissus](http://quos.org/sitet.aspx). Non color esset potest non sumit, sed vix arserat. Nisi immo silva tantum pectusque quos pennis quisquam artus!"
},
{
"__component": "shared.slider",
"files": [
"coffee-art.jpg",
"coffee-beans.jpg"
]
}
]
},
{
"title": "This shrimp is awesome",
"slug": "this-shrimp-is-awesome",
"category": {
"id": 4
},
"author": {
"id": 1
},
"description": "Mantis shrimps, or stomatopods, are marine crustaceans of the order Stomatopoda.",
"cover": null,
"blocks": [
{
"__component": "shared.rich-text",
"body": "## Probant \n\nse Lorem markdownum negat. Argo *saxa* videnda cornuaque hunc qui tanta spes teneas! Obliquis est dicenti est salutat ille tamen iuvenum nostrae dolore. - Colores nocituraque comitata eripiunt - Addit quodcunque solum cui et dextram illis - Nulli meus nec extemplo ille ferebat pressit Se blandita fulvae vox gravem Pittheus cesserunt sanguine herbis tu comitum tenuit. Sui in ruunt; Doridaque maculosae fuissem! Et loqui. \n\n## Abit sua\n\nse Lorem markdownum negat. Argo *saxa* videnda cornuaque hunc qui tanta spes teneas! Obliquis est dicenti est salutat ille tamen iuvenum nostrae dolore. - Colores nocituraque comitata eripiunt - Addit quodcunque solum cui et dextram illis - Nulli meus nec extemplo ille ferebat pressit Se blandita fulvae vox gravem Pittheus cesserunt sanguine herbis tu comitum tenuit. Sui in ruunt; Doridaque maculosae fuissem! Et loqui. "
},
{
"__component": "shared.quote",
"title": "Thelonius Monk",
"body": "You've got to dig it to dig it, you dig?"
},
{
"__component": "shared.media",
"file": "coffee-art.jpg"
},
{
"__component": "shared.rich-text",
"body": "## Spatiantia astra \n\nFoeda, medio silva *errandum*: onus formam munere. Mutata bibulis est auxiliare arces etiamnunc verbis virgineo Priamidas illa Thescelus, nam fit locis lucis auras. Exitus hospes gratulor ut pondere [speslimite](http://www.curas.io/figuram); quid habent, Avernales faciente de. Pervenit Ino sonabile supplex cognoscenti vires, Bacchumque errat miserarum venandi dignabere dedisti. Discrimina iuncosaque virgaque tot sine superest [fissus](http://quos.org/sitet.aspx). Non color esset potest non sumit, sed vix arserat. Nisi immo silva tantum pectusque quos pennis quisquam artus!"
},
{
"__component": "shared.slider",
"files": [
"coffee-art.jpg",
"coffee-beans.jpg"
]
}
]
},
{
"title": "A bug is becoming a meme on the internet",
"slug": "a-bug-is-becoming-a-meme-on-the-internet",
"category": {
"id": 2
},
"author": {
"id": 2
},
"description": "How a bug on MySQL is becoming a meme on the internet",
"cover": null,
"blocks": [
{
"__component": "shared.rich-text",
"body": "## Probant \n\nse Lorem markdownum negat. Argo *saxa* videnda cornuaque hunc qui tanta spes teneas! Obliquis est dicenti est salutat ille tamen iuvenum nostrae dolore. - Colores nocituraque comitata eripiunt - Addit quodcunque solum cui et dextram illis - Nulli meus nec extemplo ille ferebat pressit Se blandita fulvae vox gravem Pittheus cesserunt sanguine herbis tu comitum tenuit. Sui in ruunt; Doridaque maculosae fuissem! Et loqui. \n\n## Abit sua\n\nse Lorem markdownum negat. Argo *saxa* videnda cornuaque hunc qui tanta spes teneas! Obliquis est dicenti est salutat ille tamen iuvenum nostrae dolore. - Colores nocituraque comitata eripiunt - Addit quodcunque solum cui et dextram illis - Nulli meus nec extemplo ille ferebat pressit Se blandita fulvae vox gravem Pittheus cesserunt sanguine herbis tu comitum tenuit. Sui in ruunt; Doridaque maculosae fuissem! Et loqui. "
},
{
"__component": "shared.quote",
"title": "Thelonius Monk",
"body": "You've got to dig it to dig it, you dig?"
},
{
"__component": "shared.media",
"file": "coffee-art.jpg"
},
{
"__component": "shared.rich-text",
"body": "## Spatiantia astra \n\nFoeda, medio silva *errandum*: onus formam munere. Mutata bibulis est auxiliare arces etiamnunc verbis virgineo Priamidas illa Thescelus, nam fit locis lucis auras. Exitus hospes gratulor ut pondere [speslimite](http://www.curas.io/figuram); quid habent, Avernales faciente de. Pervenit Ino sonabile supplex cognoscenti vires, Bacchumque errat miserarum venandi dignabere dedisti. Discrimina iuncosaque virgaque tot sine superest [fissus](http://quos.org/sitet.aspx). Non color esset potest non sumit, sed vix arserat. Nisi immo silva tantum pectusque quos pennis quisquam artus!"
},
{
"__component": "shared.slider",
"files": [
"coffee-art.jpg",
"coffee-beans.jpg"
]
}
]
},
{
"title": "Beautiful picture",
"slug": "beautiful-picture",
"category": {
"id": 4
},
"author": {
"id": 2
},
"description": "Description of a beautiful picture",
"cover": null,
"blocks": [
{
"__component": "shared.rich-text",
"body": "## Probant \n\nse Lorem markdownum negat. Argo *saxa* videnda cornuaque hunc qui tanta spes teneas! Obliquis est dicenti est salutat ille tamen iuvenum nostrae dolore. - Colores nocituraque comitata eripiunt - Addit quodcunque solum cui et dextram illis - Nulli meus nec extemplo ille ferebat pressit Se blandita fulvae vox gravem Pittheus cesserunt sanguine herbis tu comitum tenuit. Sui in ruunt; Doridaque maculosae fuissem! Et loqui. \n\n## Abit sua\n\nse Lorem markdownum negat. Argo *saxa* videnda cornuaque hunc qui tanta spes teneas! Obliquis est dicenti est salutat ille tamen iuvenum nostrae dolore. - Colores nocituraque comitata eripiunt - Addit quodcunque solum cui et dextram illis - Nulli meus nec extemplo ille ferebat pressit Se blandita fulvae vox gravem Pittheus cesserunt sanguine herbis tu comitum tenuit. Sui in ruunt; Doridaque maculosae fuissem! Et loqui. "
},
{
"__component": "shared.quote",
"title": "Thelonius Monk",
"body": "You've got to dig it to dig it, you dig?"
},
{
"__component": "shared.media",
"file": "coffee-art.jpg"
},
{
"__component": "shared.rich-text",
"body": "## Spatiantia astra \n\nFoeda, medio silva *errandum*: onus formam munere. Mutata bibulis est auxiliare arces etiamnunc verbis virgineo Priamidas illa Thescelus, nam fit locis lucis auras. Exitus hospes gratulor ut pondere [speslimite](http://www.curas.io/figuram); quid habent, Avernales faciente de. Pervenit Ino sonabile supplex cognoscenti vires, Bacchumque errat miserarum venandi dignabere dedisti. Discrimina iuncosaque virgaque tot sine superest [fissus](http://quos.org/sitet.aspx). Non color esset potest non sumit, sed vix arserat. Nisi immo silva tantum pectusque quos pennis quisquam artus!"
},
{
"__component": "shared.slider",
"files": [
"coffee-art.jpg",
"coffee-beans.jpg"
]
}
]
},
{
"title": "What's inside a Black Hole",
"slug": "what-s-inside-a-black-hole",
"category": {
"id": 1
},
"author": {
"id": 2
},
"description": "Maybe the answer is in this article, or not...",
"cover": null,
"blocks": [
{
"__component": "shared.rich-text",
"body": "## Probant \n\nse Lorem markdownum negat. Argo *saxa* videnda cornuaque hunc qui tanta spes teneas! Obliquis est dicenti est salutat ille tamen iuvenum nostrae dolore. - Colores nocituraque comitata eripiunt - Addit quodcunque solum cui et dextram illis - Nulli meus nec extemplo ille ferebat pressit Se blandita fulvae vox gravem Pittheus cesserunt sanguine herbis tu comitum tenuit. Sui in ruunt; Doridaque maculosae fuissem! Et loqui. \n\n## Abit sua\n\nse Lorem markdownum negat. Argo *saxa* videnda cornuaque hunc qui tanta spes teneas! Obliquis est dicenti est salutat ille tamen iuvenum nostrae dolore. - Colores nocituraque comitata eripiunt - Addit quodcunque solum cui et dextram illis - Nulli meus nec extemplo ille ferebat pressit Se blandita fulvae vox gravem Pittheus cesserunt sanguine herbis tu comitum tenuit. Sui in ruunt; Doridaque maculosae fuissem! Et loqui. "
},
{
"__component": "shared.quote",
"title": "Thelonius Monk",
"body": "You've got to dig it to dig it, you dig?"
},
{
"__component": "shared.media",
"file": "coffee-art.jpg"
},
{
"__component": "shared.rich-text",
"body": "## Spatiantia astra \n\nFoeda, medio silva *errandum*: onus formam munere. Mutata bibulis est auxiliare arces etiamnunc verbis virgineo Priamidas illa Thescelus, nam fit locis lucis auras. Exitus hospes gratulor ut pondere [speslimite](http://www.curas.io/figuram); quid habent, Avernales faciente de. Pervenit Ino sonabile supplex cognoscenti vires, Bacchumque errat miserarum venandi dignabere dedisti. Discrimina iuncosaque virgaque tot sine superest [fissus](http://quos.org/sitet.aspx). Non color esset potest non sumit, sed vix arserat. Nisi immo silva tantum pectusque quos pennis quisquam artus!"
},
{
"__component": "shared.slider",
"files": [
"coffee-art.jpg",
"coffee-beans.jpg"
]
}
]
}
]
}
================================================
FILE: packages/templates/blog/template/src/api/.gitkeep
================================================
================================================
FILE: packages/templates/blog/template/src/api/about/content-types/about/schema.json
================================================
{
"kind": "singleType",
"collectionName": "abouts",
"info": {
"singularName": "about",
"pluralName": "abouts",
"displayName": "About",
"description": "Write about yourself and the content you create"
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"title": {
"type": "string"
},
"blocks": {
"type": "dynamiczone",
"components": [
"shared.media",
"shared.quote",
"shared.rich-text",
"shared.slider"
]
}
}
}
================================================
FILE: packages/templates/blog/template/src/api/about/controllers/about.js
================================================
'use strict';
/**
* about controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::about.about');
================================================
FILE: packages/templates/blog/template/src/api/about/routes/about.js
================================================
'use strict';
/**
* about router.
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::about.about');
================================================
FILE: packages/templates/blog/template/src/api/about/services/about.js
================================================
'use strict';
/**
* about service.
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::about.about');
================================================
FILE: packages/templates/blog/template/src/api/article/content-types/article/schema.json
================================================
{
"kind": "collectionType",
"collectionName": "articles",
"info": {
"singularName": "article",
"pluralName": "articles",
"displayName": "Article",
"description": "Create your blog content"
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {},
"attributes": {
"title": {
"type": "string"
},
"description": {
"type": "text",
"maxLength": 80
},
"slug": {
"type": "uid",
"targetField": "title"
},
"cover": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images",
"files",
"videos"
]
},
"author": {
"type": "relation",
"relation": "manyToOne",
"target": "api::author.author",
"inversedBy": "articles"
},
"category": {
"type": "relation",
"relation": "manyToOne",
"target": "api::category.category",
"inversedBy": "articles"
},
"blocks": {
"type": "dynamiczone",
"components": [
"shared.media",
"shared.quote",
"shared.rich-text",
"shared.slider"
]
}
}
}
================================================
FILE: packages/templates/blog/template/src/api/article/controllers/article.js
================================================
'use strict';
/**
* article controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::article.article');
================================================
FILE: packages/templates/blog/template/src/api/article/routes/article.js
================================================
'use strict';
/**
* article router.
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::article.article');
================================================
FILE: packages/templates/blog/template/src/api/article/services/article.js
================================================
'use strict';
/**
* article service.
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::article.article');
================================================
FILE: packages/templates/blog/template/src/api/author/content-types/author/schema.json
================================================
{
"kind": "collectionType",
"collectionName": "authors",
"info": {
"singularName": "author",
"pluralName": "authors",
"displayName": "Author",
"description": "Create authors for your content"
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string"
},
"avatar": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images",
"files",
"videos"
]
},
"email": {
"type": "string"
},
"articles": {
"type": "relation",
"relation": "oneToMany",
"target": "api::article.article",
"mappedBy": "author"
}
}
}
================================================
FILE: packages/templates/blog/template/src/api/author/controllers/author.js
================================================
'use strict';
/**
* author controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::author.author');
================================================
FILE: packages/templates/blog/template/src/api/author/routes/author.js
================================================
'use strict';
/**
* author router.
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::author.author');
================================================
FILE: packages/templates/blog/template/src/api/author/services/author.js
================================================
'use strict';
/**
* author service.
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::author.author');
================================================
FILE: packages/templates/blog/template/src/api/category/content-types/category/schema.json
================================================
{
"kind": "collectionType",
"collectionName": "categories",
"info": {
"singularName": "category",
"pluralName": "categories",
"displayName": "Category",
"description": "Organize your content into categories"
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"name": {
"type": "string"
},
"slug": {
"type": "uid"
},
"articles": {
"type": "relation",
"relation": "oneToMany",
"target": "api::article.article",
"mappedBy": "category"
},
"description": {
"type": "text"
}
}
}
================================================
FILE: packages/templates/blog/template/src/api/category/controllers/category.js
================================================
'use strict';
/**
* category controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::category.category');
================================================
FILE: packages/templates/blog/template/src/api/category/routes/category.js
================================================
'use strict';
/**
* category router.
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::category.category');
================================================
FILE: packages/templates/blog/template/src/api/category/services/category.js
================================================
'use strict';
/**
* category service.
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::category.category');
================================================
FILE: packages/templates/blog/template/src/api/global/content-types/global/schema.json
================================================
{
"kind": "singleType",
"collectionName": "globals",
"info": {
"singularName": "global",
"pluralName": "globals",
"displayName": "Global",
"description": "Define global settings"
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {},
"attributes": {
"siteName": {
"type": "string",
"required": true
},
"favicon": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images",
"files",
"videos"
]
},
"siteDescription": {
"type": "text",
"required": true
},
"defaultSeo": {
"type": "component",
"repeatable": false,
"component": "shared.seo"
}
}
}
================================================
FILE: packages/templates/blog/template/src/api/global/controllers/global.js
================================================
'use strict';
/**
* global controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::global.global');
================================================
FILE: packages/templates/blog/template/src/api/global/routes/global.js
================================================
'use strict';
/**
* global router.
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::global.global');
================================================
FILE: packages/templates/blog/template/src/api/global/services/global.js
================================================
'use strict';
/**
* global service.
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::global.global');
================================================
FILE: packages/templates/blog/template/src/bootstrap.js
================================================
"use strict";
const fs = require("fs-extra");
const path = require("path");
const mime = require("mime-types");
const set = require("lodash.set");
const {
categories,
authors,
articles,
global,
about,
} = require("../data/data.json");
async function isFirstRun() {
const pluginStore = strapi.store({
environment: strapi.config.environment,
type: "type",
name: "setup",
});
const initHasRun = await pluginStore.get({ key: "initHasRun" });
await pluginStore.set({ key: "initHasRun", value: true });
return !initHasRun;
}
async function setPublicPermissions(newPermissions) {
// Find the ID of the public role
const publicRole = await strapi
.query("plugin::users-permissions.role")
.findOne({
where: {
type: "public",
},
});
// Create the new permissions and link them to the public role
const allPermissionsToCreate = [];
Object.keys(newPermissions).map((controller) => {
const actions = newPermissions[controller];
const permissionsToCreate = actions.map((action) => {
return strapi.query("plugin::users-permissions.permission").create({
data: {
action: `api::${controller}.${controller}.${action}`,
role: publicRole.id,
},
});
});
allPermissionsToCreate.push(...permissionsToCreate);
});
await Promise.all(allPermissionsToCreate);
}
function getFileSizeInBytes(filePath) {
const stats = fs.statSync(filePath);
const fileSizeInBytes = stats["size"];
return fileSizeInBytes;
}
function getFileData(fileName) {
const filePath = path.join("data", "uploads", fileName);
// Parse the file metadata
const size = getFileSizeInBytes(filePath);
const ext = fileName.split(".").pop();
const mimeType = mime.lookup(ext);
return {
path: filePath,
name: fileName,
size,
type: mimeType,
};
}
async function uploadFile(file, name) {
return strapi
.plugin("upload")
.service("upload")
.upload({
files: file,
data: {
fileInfo: {
alternativeText: `An image uploaded to Strapi called ${name}`,
caption: name,
name,
},
},
});
}
// Create an entry and attach files if there are any
async function createEntry({ model, entry }) {
try {
// Actually create the entry in Strapi
await strapi.entityService.create(`api::${model}.${model}`, {
data: entry,
});
} catch (error) {
console.error({ model, entry, error });
}
}
async function checkFileExistsBeforeUpload(files) {
const existingFiles = [];
const uploadedFiles = [];
const filesCopy = [...files];
for (const fileName of filesCopy) {
// Check if the file already exists in Strapi
const fileWhereName = await strapi.query("plugin::upload.file").findOne({
where: {
name: fileName,
},
});
if (fileWhereName) {
// File exists, don't upload it
existingFiles.push(fileWhereName);
} else {
// File doesn't exist, upload it
const fileData = getFileData(fileName);
const fileNameNoExtension = fileName.split('.').shift()
const [file] = await uploadFile(fileData, fileNameNoExtension);
uploadedFiles.push(file);
}
}
const allFiles = [...existingFiles, ...uploadedFiles];
// If only one file then return only that file
return allFiles.length === 1 ? allFiles[0] : allFiles;
}
async function updateBlocks(blocks) {
const updatedBlocks = [];
for (const block of blocks) {
if (block.__component === "shared.media") {
const uploadedFiles = await checkFileExistsBeforeUpload([block.file]);
// Copy the block to not mutate directly
const blockCopy = { ...block };
// Replace the file name on the block with the actual file
blockCopy.file = uploadedFiles;
updatedBlocks.push(blockCopy);
} else if (block.__component === "shared.slider") {
// Get files already uploaded to Strapi or upload new files
const existingAndUploadedFiles = await checkFileExistsBeforeUpload(
block.files
);
// Copy the block to not mutate directly
const blockCopy = { ...block };
// Replace the file names on the block with the actual files
blockCopy.files = existingAndUploadedFiles;
// Push the updated block
updatedBlocks.push(blockCopy);
} else {
// Just push the block as is
updatedBlocks.push(block);
}
}
return updatedBlocks;
}
async function importArticles() {
for (const article of articles) {
const cover = await checkFileExistsBeforeUpload([`${article.slug}.jpg`]);
const updatedBlocks = await updateBlocks(article.blocks);
await createEntry({
model: "article",
entry: {
...article,
cover,
blocks: updatedBlocks,
// Make sure it's not a draft
publishedAt: Date.now(),
},
});
}
}
async function importGlobal() {
const favicon = await checkFileExistsBeforeUpload(["favicon.png"]);
const shareImage = await checkFileExistsBeforeUpload(["default-image.png"])
return createEntry({
model: "global",
entry: {
...global,
favicon,
// Make sure it's not a draft
publishedAt: Date.now(),
defaultSeo: {
...global.defaultSeo,
shareImage
}
},
});
}
async function importAbout() {
const updatedBlocks = await updateBlocks(about.blocks);
await createEntry({
model: "about",
entry: {
...about,
blocks: updatedBlocks,
// Make sure it's not a draft
publishedAt: Date.now(),
},
});
}
async function importCategories() {
for (const category of categories) {
await createEntry({ model: "category", entry: category });
}
}
async function importAuthors() {
for (const author of authors) {
const avatar = await checkFileExistsBeforeUpload([author.avatar]);
await createEntry({
model: "author",
entry: {
...author,
avatar,
},
});
}
}
async function importSeedData() {
// Allow read of application content types
await setPublicPermissions({
article: ["find", "findOne"],
category: ["find", "findOne"],
author: ["find", "findOne"],
global: ["find", "findOne"],
about: ["find", "findOne"],
});
// Create all entries
await importCategories();
await importAuthors();
await importArticles();
await importGlobal();
await importAbout();
}
module.exports = async () => {
const shouldImportSeedData = await isFirstRun();
if (shouldImportSeedData) {
try {
console.log("Setting up the template...");
await importSeedData();
console.log("Ready to go");
} catch (error) {
console.log("Could not import seed data");
console.error(error);
}
}
};
================================================
FILE: packages/templates/blog/template/src/components/shared/media.json
================================================
{
"collectionName": "components_shared_media",
"info": {
"displayName": "Media",
"icon": "file-video"
},
"options": {},
"attributes": {
"file": {
"allowedTypes": [
"images",
"files",
"videos"
],
"type": "media",
"multiple": false
}
}
}
================================================
FILE: packages/templates/blog/template/src/components/shared/quote.json
================================================
{
"collectionName": "components_shared_quotes",
"info": {
"displayName": "Quote",
"icon": "indent"
},
"options": {},
"attributes": {
"title": {
"type": "string"
},
"body": {
"type": "text"
}
}
}
================================================
FILE: packages/templates/blog/template/src/components/shared/rich-text.json
================================================
{
"collectionName": "components_shared_rich_texts",
"info": {
"displayName": "Rich text",
"icon": "align-justify",
"description": ""
},
"options": {},
"attributes": {
"body": {
"type": "richtext"
}
}
}
================================================
FILE: packages/templates/blog/template/src/components/shared/seo.json
================================================
{
"collectionName": "components_shared_seos",
"info": {
"name": "Seo",
"icon": "allergies",
"displayName": "Seo",
"description": ""
},
"options": {},
"attributes": {
"metaTitle": {
"type": "string",
"required": true
},
"metaDescription": {
"type": "text",
"required": true
},
"shareImage": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
]
}
}
}
================================================
FILE: packages/templates/blog/template/src/components/shared/slider.json
================================================
{
"collectionName": "components_shared_sliders",
"info": {
"displayName": "Slider",
"icon": "address-book",
"description": ""
},
"options": {},
"attributes": {
"files": {
"type": "media",
"multiple": true,
"required": false,
"allowedTypes": [
"images"
]
}
}
}
================================================
FILE: packages/templates/blog/template/src/extensions/.gitkeep
================================================
================================================
FILE: packages/templates/blog/template/src/index.js
================================================
"use strict";
const bootstrap = require("./bootstrap");
module.exports = {
/**
* An asynchronous register function that runs before
* your application is initialized.
*
* This gives you an opportunity to extend code.
*/
register(/*{ strapi }*/) {},
/**
* An asynchronous bootstrap function that runs before
* your application gets started.
*
* This gives you an opportunity to set up your data model,
* run jobs, or perform some special logic.
*/
bootstrap,
};
================================================
FILE: packages/templates/blog/template.json
================================================
{
"package": {
"dependencies": {
"@strapi/plugin-graphql": "^4.0.5",
"fs-extra": "^10.0.0",
"lodash.set": "^4.3.2",
"mime-types": "^2.1.27"
}
}
}
================================================
FILE: packages/templates/corporate/README.md
================================================
# strapi-template-corporate
A Strapi template to create Strapi projects pre-configured for corporate apps.
## Usage
```bash
# Using Yarn
yarn create strapi-app my-app-name --template corporate
# Or using NPM
npx create-strapi-app my-app-name --template corporate
```
## Starters
This template is used by the following starters:
* [Strapi Starter Next Corporate Site](https://github.com/strapi/strapi-starter-next-corporate)
* [Strapi Starter Gatsby Corporate Site](https://github.com/strapi/strapi-starter-gatsby-corporate)
================================================
FILE: packages/templates/corporate/package.json
================================================
{
"name": "@strapi/template-corporate",
"version": "1.0.0",
"description": "Strapi corporate template",
"keywords": [
"strapi",
"template",
"corporate"
],
"homepage": "https://github.com/strapi/starters-and-templates#readme",
"bugs": {
"url": "https://github.com/strapi/starters-and-templates/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/strapi/starters-and-templates.git"
},
"license": "MIT",
"author": {
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
},
"maintainers": [
{
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
}
],
"gitHead": "d6d5bd264d1df0a0f23b5234d84348f8d78882e3"
}
================================================
FILE: packages/templates/corporate/template/data/data.js
================================================
const { global, pages } = require("./en");
const { globalFR, pagesFR } = require("./fr");
const { leadFormSubmissions } = require("./lead-form-submissions.json");
module.exports = {
globals: [global, globalFR],
pages: [...pages, ...pagesFR],
leadFormSubmissions,
};
================================================
FILE: packages/templates/corporate/template/data/en/global.json
================================================
{
"global": {
"id": 1,
"locale": "en",
"metaTitleSuffix": "Strapi Corporate",
"metadata": {
"id": 1,
"metaTitle": "Strapi starter for Corporate Sites",
"metaDescription": "Build your corporate site with Strapi",
"twitterCardType": "summary",
"twitterUsername": "strapijs",
"shareImage": null
},
"notificationBanner": {
"id": 1,
"text": "This page was built using the Strapi starter for Corporate Sites.\n[View other Strapi starters](https://strapi.io/starters)",
"type": "info"
},
"navbar": {
"id": 1,
"links": [
{
"id": 1,
"url": "/pricing",
"newTab": false,
"text": "Pricing"
},
{
"id": 3,
"url": "/contact",
"newTab": false,
"text": "Contact"
}
],
"button": {
"id": 13,
"url": "#",
"newTab": false,
"text": "Sign up",
"type": "secondary"
},
"logo" : null
},
"footer": {
"id": 1,
"smallText": "© Copyright My Company™",
"columns": [
{
"id": 1,
"title": "Product",
"links": [
{
"id": 11,
"url": "#",
"newTab": false,
"text": "Features"
},
{
"id": 13,
"url": "#",
"newTab": false,
"text": "Sign up"
}
]
},
{
"id": 2,
"title": "Legal",
"links": [
{
"id": 15,
"url": "#",
"newTab": false,
"text": "Privacy policy"
},
{
"id": 16,
"url": "#",
"newTab": false,
"text": "Terms & conditions"
}
]
},
{
"id": 5,
"title": "Company",
"links": [
{
"id": 18,
"url": "#",
"newTab": false,
"text": "Careers"
},
{
"id": 21,
"url": "#",
"newTab": false,
"text": "Team"
}
]
},
{
"id": 7,
"title": "Social",
"links": [
{
"id": 24,
"url": "#",
"newTab": false,
"text": "Twitter"
},
{
"id": 25,
"url": "#",
"newTab": false,
"text": "LinkedIn"
}
]
}
],
"logo": null
},
"favicon": null,
"localizations": [
{
"id": 2,
"locale": "fr"
}
]
}
}
================================================
FILE: packages/templates/corporate/template/data/en/index.js
================================================
const { global } = require("./global.json");
const { pages } = require("./pages.json");
module.exports = {
global,
pages
}
================================================
FILE: packages/templates/corporate/template/data/en/pages.json
================================================
{
"pages": [
{
"id": 1,
"slug": "",
"locale": "en",
"localizations": [
{
"id": 5,
"locale": "fr"
}
],
"shortName": "Home",
"publishedAt": "2021-10-22T08:11:55.490Z",
"metadata": {
"id": 2,
"metaTitle": "Strapi corporate site starter",
"metaDescription": "Build a fully editable site with Strapi",
"twitterCardType": "summary",
"twitterUsername": null,
"shareImage": null
},
"contentSections": [
{
"id": 1,
"__component": "sections.hero",
"title": "The best way to build your Corporate Site",
"label": "New Strapi starter",
"description": "Get started with your Strapi business website in seconds.",
"smallTextWithLink": "Want to build your own from scratch? Tutorial coming soon",
"buttons": [
{
"id": 2,
"url": "https://github.com/strapi/strapi-template-corporate",
"newTab": false,
"text": "Get started",
"type": "primary"
},
{
"id": 4,
"url": "https://github.com/strapi/strapi-template-corporate",
"newTab": true,
"text": "See the code",
"type": "secondary"
}
],
"picture": null
},
{
"id": 1,
"__component": "sections.feature-rows-group",
"features": [
{
"id": 1,
"title": "Let marketing teams build their pages",
"description": "Thanks to a pre-built list of UI sections, marketers can design exactly the pages they want.",
"link": {
"id": 7,
"url": "#",
"newTab": false,
"text": "View the list of UI sections"
},
"media": null
},
{
"id": 2,
"title": "Leave your developers alone",
"description": "You can publish, edit and delete pages without help from developers. Your technical team can finally focus on their tasks.",
"link": {
"id": 8,
"url": "#",
"newTab": false,
"text": "View how to create a page"
},
"media": null
}
]
},
{
"id": 1,
"__component": "sections.feature-columns-group",
"features": [
{
"id": 1,
"title": "Preview your changes",
"description": "Thanks to an integrated Preview Mode, you can visualize your pages before publishing them.",
"icon": null
},
{
"id": 2,
"title": "Fully responsive",
"description": "This starter works well on all screens, whether it's mobile, tablet or desktop.",
"icon": null
},
{
"id": 3,
"title": "Easy to customize",
"description": "We use Tailwind for styling. You can change your site's theme without digging through the code.",
"icon": null
}
]
},
{
"id": 1,
"__component": "sections.testimonials-group",
"title": "Your customer testimonials here",
"description": "This section is where you can showcase your customers. Insert quotes, and show the logos of companies who like your product",
"link": {
"id": 2,
"url": "#",
"newTab": false,
"text": "All testimonials"
},
"logos": [
{
"id": 1,
"title": "Strapi",
"logo": null
},
{
"id": 2,
"title": "Strapi 2",
"logo": null
},
{
"id": 3,
"title": "Strapi 3",
"logo": null
}
],
"testimonials": [
{
"id": 1,
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
"authorName": "Your Customer Name",
"authorTitle": "A happy customer",
"link": "#",
"logo": null,
"picture": null
}
]
},
{
"id": 1,
"__component": "sections.lead-form",
"title": "Subscribe To Our Newsletter",
"emailPlaceholder": "email@company.com",
"submitButton": {
"id": 1,
"__component": "links.button",
"text": "Subscribe",
"type": "primary"
},
"location": "Home Page Bottom"
},
{
"id": 2,
"__component": "sections.bottom-actions",
"title": "Start building your website",
"buttons": [
{
"id": 7,
"url": "https://github.com/strapi/strapi-template-corporate",
"newTab": false,
"text": "Get started",
"type": "primary"
},
{
"id": 8,
"url": "https://github.com/strapi/strapi-template-corporate",
"newTab": false,
"text": "See the code",
"type": "secondary"
}
]
}
]
},
{
"id": 2,
"slug": "pricing",
"locale": "en",
"localizations": [
{
"id": 6,
"locale": "fr"
}
],
"shortName": "Pricing",
"publishedAt": "2021-10-22T08:11:55.490Z",
"metadata": {
"id": 3,
"metaTitle": "Pricing",
"metaDescription": "The different plans",
"twitterCardType": "summary",
"twitterUsername": null,
"shareImage": null
},
"contentSections": [
{
"id": 1,
"__component": "sections.pricing",
"title": "Clear pricing table",
"plans": [
{
"id": 1,
"name": "Hobby",
"description": "Perfect for side projects",
"isRecommended": null,
"price": 0,
"pricePeriod": "forever",
"features": [
{
"id": 1,
"name": "A cool feature"
},
{
"id": 2,
"name": "Another cool feature"
},
{
"id": 3,
"name": "Some other cool feature"
}
]
},
{
"id": 2,
"name": "Premium",
"description": "A more advanced plan for SMBs",
"isRecommended": true,
"price": 20,
"pricePeriod": "per month",
"features": [
{
"id": 4,
"name": "The coolest feature"
},
{
"id": 5,
"name": "Nice feature"
},
{
"id": 6,
"name": "Fun feature"
}
]
},
{
"id": 3,
"name": "Enterprise",
"description": "For large companies needs",
"isRecommended": null,
"price": 299,
"pricePeriod": "per month",
"features": [
{
"id": 7,
"name": "Amazing feature"
},
{
"id": 8,
"name": "Wow effect feature"
},
{
"id": 9,
"name": "Mesmerizing feature"
}
]
}
]
}
]
},
{
"id": 3,
"slug": "secret",
"locale": "en",
"localizations": [
{
"id": 7,
"locale": "fr"
}
],
"shortName": "Secret",
"metadata": {
"id": 7,
"metaTitle": "Secret page",
"metaDescription": "Preview-only page",
"twitterCardType": "summary",
"twitterUsername": null,
"shareImage": null
},
"contentSections": [
{
"id": 2,
"__component": "sections.rich-text",
"content": "## Secret page\n\nYou can only view this page in Preview Mode."
}
]
},
{
"id": 4,
"slug": "contact",
"locale": "en",
"localizations": [
{
"id": 8,
"locale": "fr"
}
],
"shortName": "Contact",
"publishedAt": "2021-10-22T08:11:55.490Z",
"metadata": {
"id": 4,
"metaTitle": "Contact",
"metaDescription": "Get in touch with our team",
"twitterCardType": "summary",
"twitterUsername": null,
"shareImage": null
},
"contentSections": [
{
"id": 1,
"__component": "sections.rich-text",
"content": "# Get in touch\n\n> This is an example of a page that relies almost entirely on the RichText section. It's useful for blog articles, or content-heavy pages like legal terms.\n\nWe'd love to hear from you.\n\n## Social media\n\n* [Twitter](#)\n* [Twitter](#)\n* [Twitter](#)\n\n## Postal address\n\n404 Headless Street\n__92210__ **Saint Cloud**, *France*\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
},
{
"id": 1,
"__component": "sections.bottom-actions",
"title": "Get in touch",
"buttons": [
{
"id": 1,
"url": "#",
"newTab": false,
"text": "Send an email",
"type": "primary"
},
{
"id": 3,
"url": "#",
"newTab": false,
"text": "DM us on Twitter",
"type": "secondary"
}
]
}
]
}
]
}
================================================
FILE: packages/templates/corporate/template/data/fr/global.json
================================================
{
"globalFR": {
"id": 2,
"locale": "fr",
"metaTitleSuffix": "Strapi Corporate",
"metadata": {
"id": 5,
"metaTitle": "Strapi starter for Corporate Sites",
"metaDescription": "Créer votre site corporate avec Strapi",
"twitterCardType": "summary",
"twitterUsername": "strapijs",
"shareImage": null
},
"notificationBanner": {
"id": 2,
"text": "Cette page a été crée avec le Strapi starter pour les sites Corporate.\n [Voir les autres starters Strapi](https://strapi.io/starters)",
"type": "info"
},
"navbar": {
"id": 2,
"links": [
{
"id": 4,
"url": "/pricing",
"newTab": false,
"text": "tarifs"
},
{
"id": 5,
"url": "/contact",
"newTab": false,
"text": "nous contacter"
}
],
"button": {
"id": 14,
"url": "#",
"newTab": false,
"text": "S'inscrire",
"type": "secondary"
},
"logo" : null
},
"footer": {
"id": 2,
"smallText": "© Copyright Mon Entreprise™",
"columns": [
{
"id": 3,
"title": "Produit",
"links": [
{
"id": 12,
"url": "#",
"newTab": false,
"text": "Fonctionnalités"
},
{
"id": 14,
"url": "#",
"newTab": false,
"text": "S'inscrire"
}
]
},
{
"id": 4,
"title": "Légal",
"links": [
{
"id": 17,
"url": "#",
"newTab": false,
"text": "Confidentialité"
},
{
"id": 19,
"url": "#",
"newTab": false,
"text": "Mentions légales"
}
]
},
{
"id": 6,
"title": "Entreprise",
"links": [
{
"id": 20,
"url": "#",
"newTab": false,
"text": "Nous rejoindre"
},
{
"id": 22,
"url": "#",
"newTab": false,
"text": "L'équipe"
}
]
},
{
"id": 8,
"title": "Réseaux sociaux",
"links": [
{
"id": 23,
"url": "#",
"newTab": false,
"text": "Twitter"
},
{
"id": 26,
"url": "#",
"newTab": false,
"text": "LinkedIn"
}
]
}
],
"logo": null
},
"favicon": null,
"localizations": [
{
"id": 1,
"locale": "en"
}
]
}
}
================================================
FILE: packages/templates/corporate/template/data/fr/index.js
================================================
const { globalFR } = require("./global.json");
const { pagesFR } = require("./pages.json");
module.exports = {
globalFR,
pagesFR
}
================================================
FILE: packages/templates/corporate/template/data/fr/pages.json
================================================
{
"pagesFR": [
{
"id": 5,
"slug": "",
"locale": "fr",
"localizations": [
{
"id": 1,
"locale": "en"
}
],
"shortName": "Home",
"publishedAt":"2021-10-22T08:11:55.490Z",
"metadata": {
"id": 6,
"metaTitle": "Strapi corporate site starter",
"metaDescription": "Construire un site modifiable avec Strapi",
"twitterCardType": "summary",
"twitterUsername": null,
"shareImage": null
},
"contentSections": [
{
"id": 2,
"__component": "sections.hero",
"title": "La meilleure façon de faire votre site Corporate",
"label": "Nouveau starter Strapi",
"description": "Commencer vite avec votre site corporate",
"smallTextWithLink": "Vous voulez construire votre propre starter? Guide à venir",
"buttons": [
{
"id": 5,
"url": "https://github.com/strapi/strapi-template-corporate",
"newTab": false,
"text": "Commencer",
"type": "primary"
},
{
"id": 6,
"url": "https://github.com/strapi/strapi-template-corporate",
"newTab": true,
"text": "Voir le code",
"type": "secondary"
}
],
"picture": null
},
{
"id": 2,
"__component": "sections.feature-rows-group",
"features": [
{
"id": 3,
"title": "Les équipes marketing peuvent maintenant faire leurs propres pages",
"description": "Grace à une liste pré-construite de blocs UI, les équipes marketing peuvent faire la mise en page exactement comme elles veulent",
"link": {
"id": 9,
"url": "#",
"newTab": false,
"text": "Voir la liste de blocs UI"
},
"media": null
},
{
"id": 4,
"title": "Laissez vos développeurs tranquilles",
"description": "Vous pouvez publier, modifier, et supprimer des pages sans l'aide d'un développeur. Votre équipe technique peut enfin se concentrer sur d'autre tâches.",
"link": {
"id": 10,
"url": "#",
"newTab": false,
"text": "Voir comment créer une page"
},
"media": null
}
]
},
{
"id": 2,
"__component": "sections.feature-columns-group",
"features": [
{
"id": 4,
"title": "Visualisez vos changments",
"description": "Grace à un mode preview, vous pouvez voir vos pages avant de les publier.",
"icon": null
},
{
"id": 5,
"title": "100% responsive",
"description": "Ce starter marche sur toutes les tailles d'écrans.",
"icon": null
},
{
"id": 6,
"title": "Facile à personnaliser",
"description": "On utilise Tailwind pour les styles. Vous pouvez changez le thème de votre site sans avoir besoin d'aller dans le code.",
"icon": null
}
]
},
{
"id": 2,
"__component": "sections.testimonials-group",
"title": "Ici, les témoignages des vos clients.",
"description": "Dans cette partie, vous pouvez mettre en avant vos clients. Mettez des citations, et montrez les logos des entreprises qui aiment votre produit.",
"link": {
"id": 6,
"url": "#",
"newTab": false,
"text": "All testimonials"
},
"logos": [
{
"id": 4,
"title": "Strapi",
"logo": null
},
{
"id": 5,
"title": "Strapi 2",
"logo": null
},
{
"id": 6,
"title": "Strapi 3",
"logo": null
}
],
"testimonials": [
{
"id": 2,
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
"authorName": "Nom d'un(e) Client(e) ici",
"authorTitle": "Un(e) Client(e) Satisfait(e)",
"link": "#",
"logo": null,
"picture": null
}
]
},
{
"id": 2,
"__component": "sections.lead-form",
"title": "S'inscrire à notre Newsletter",
"emailPlaceholder": "email@company.com",
"submitButton": {
"id": 2,
"__component": "links.button",
"text": "S'inscrire",
"type": "primary"
},
"location": "Home Page Bottom"
},
{
"id": 3,
"__component": "sections.bottom-actions",
"title": "Start building your website",
"buttons": [
{
"id": 10,
"url": "https://github.com/strapi/strapi-template-corporate",
"newTab": false,
"text": "Commencer",
"type": "primary"
},
{
"id": 12,
"url": "https://github.com/strapi/strapi-template-corporate",
"newTab": false,
"text": "Voir le code",
"type": "secondary"
}
]
}
]
},
{
"id": 6,
"slug": "pricing",
"locale": "fr",
"localizations": [
{
"id": 2,
"locale": "en"
}
],
"shortName": "Pricing",
"publishedAt":"2021-10-22T08:11:55.490Z",
"metadata": {
"id": 8,
"metaTitle": "Pricing",
"metaDescription": "Les différentes offres",
"twitterCardType": "summary",
"twitterUsername": null,
"shareImage": null
},
"contentSections": [
{
"id": 2,
"__component": "sections.pricing",
"title": "Tableau de tarifs",
"plans": [
{
"id": 4,
"name": "Hobby",
"description": "Parfait pour des projets personnels",
"isRecommended": null,
"price": 0,
"pricePeriod": "toujours",
"features": [
{
"id": 10,
"name": "Voilà une feature"
},
{
"id": 11,
"name": "Une autre"
},
{
"id": 12,
"name": "Encore une autre"
}
]
},
{
"id": 5,
"name": "Premium",
"description": "Un plan plus avancé",
"isRecommended": true,
"price": 20,
"pricePeriod": "par mois",
"features": [
{
"id": 13,
"name": "Fonctionnalité 1"
},
{
"id": 14,
"name": "Fonctionnalité 2"
},
{
"id": 15,
"name": "Fonctionnalité 3"
}
]
},
{
"id": 6,
"name": "Entreprise",
"description": "Pour les besoin des grands entreprises",
"isRecommended": null,
"price": 299,
"pricePeriod": "par mois",
"features": [
{
"id": 16,
"name": "Fonctionnalité 1"
},
{
"id": 17,
"name": "Fonctionnalité 2"
},
{
"id": 18,
"name": "Fonctionnalité 3"
}
]
}
]
}
]
},
{
"id": 7,
"slug": "secret",
"locale": "fr",
"localizations": [
{
"id": 3,
"locale": "en"
}
],
"shortName": "Secret",
"metadata": {
"id": 9,
"metaTitle": "Page secrète",
"metaDescription": "Preview-only page",
"twitterCardType": "summary",
"twitterUsername": null,
"shareImage": null
},
"contentSections": [
{
"id": 3,
"__component": "sections.rich-text",
"content": "## Page secrète. \n\n Vous seul pouvez voir cette page en Preview Mode"
}
]
},
{
"id": 8,
"slug": "contact",
"locale": "fr",
"localizations": [
{
"id": 4,
"locale": "en"
}
],
"shortName": "Contact",
"publishedAt":"2021-10-22T08:11:55.490Z",
"metadata": {
"id": 10,
"metaTitle": "Nous Contacter",
"metaDescription": "Contactez notre équipe",
"twitterCardType": "summary",
"twitterUsername": null,
"shareImage": null
},
"contentSections": [
{
"id": 4,
"__component": "sections.rich-text",
"content": "# Contactez nous\n\n> Ceci est un exemple d'une page qui repose presque entièrement sur le composant RichText. Vous pouvez l'utiliser pour des articles de blog, ou pour des pages comportant beaucoup de texte, comme par exemple les conditions d'utilisation.\n\nNous aimerions beaucoup avoir vos retours.\n\n## Social media\n\n* [Twitter](#)\n* [Twitter](#)\n* [Twitter](#)\n\n## Postal address\n\n404 Headless Street\n__92210__ **Saint Cloud**, *France*\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
},
{
"id": 4,
"__component": "sections.bottom-actions",
"title": "Get in touch",
"buttons": [
{
"id": 9,
"url": "#",
"newTab": false,
"text": "Envoyer un mail",
"type": "primary"
},
{
"id": 11,
"url": "#",
"newTab": false,
"text": "DM nous sur Twitter",
"type": "secondary"
}
]
}
]
}
]
}
================================================
FILE: packages/templates/corporate/template/data/lead-form-submissions.json
================================================
{
"leadFormSubmissions": [
{
"email": "johne@thebeatles.com",
"status": "seen",
"location": "Home Page Bottom"
},
{
"email": "paul@thebeatles.com",
"status": "contacted",
"location": "Home Page Bottom"
},
{
"email": "george@thebeatles.com",
"status": "ignored",
"location": "Home Page Bottom"
}
]
}
================================================
FILE: packages/templates/corporate/template/src/api/.gitkeep
================================================
================================================
FILE: packages/templates/corporate/template/src/api/global/content-types/global/schema.json
================================================
{
"kind": "singleType",
"collectionName": "globals",
"info": {
"singularName": "global",
"pluralName": "globals",
"displayName": "Global",
"name": "global"
},
"options": {
"increments": true,
"timestamps": true,
"draftAndPublish": false
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"metadata": {
"type": "component",
"repeatable": false,
"component": "meta.metadata",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"metaTitleSuffix": {
"type": "string",
"required": true,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"favicon": {
"allowedTypes": [
"images"
],
"type": "media",
"multiple": false,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"notificationBanner": {
"type": "component",
"repeatable": false,
"component": "elements.notification-banner",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"navbar": {
"type": "component",
"repeatable": false,
"component": "layout.navbar",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"footer": {
"type": "component",
"repeatable": false,
"component": "layout.footer",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
================================================
FILE: packages/templates/corporate/template/src/api/global/controllers/global.js
================================================
'use strict';
/**
* global controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::global.global');
================================================
FILE: packages/templates/corporate/template/src/api/global/routes/global.js
================================================
'use strict';
/**
* global router.
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::global.global');
================================================
FILE: packages/templates/corporate/template/src/api/global/services/global.js
================================================
'use strict';
/**
* global service.
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::global.global');
================================================
FILE: packages/templates/corporate/template/src/api/lead-form-submission/content-types/lead-form-submission/schema.json
================================================
{
"kind": "collectionType",
"collectionName": "lead_form_submissions",
"info": {
"singularName": "lead-form-submission",
"pluralName": "lead-form-submissions",
"displayName": "Lead form submission",
"name": "lead-form-submission"
},
"options": {
"increments": true,
"timestamps": true,
"draftAndPublish": false
},
"attributes": {
"email": {
"type": "string"
},
"status": {
"type": "enumeration",
"enum": [
"seen",
"contacted",
"ignored"
]
},
"location": {
"type": "string"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/api/lead-form-submission/controllers/lead-form-submission.js
================================================
'use strict';
/**
* lead-form-submission controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::lead-form-submission.lead-form-submission');
================================================
FILE: packages/templates/corporate/template/src/api/lead-form-submission/routes/lead-form-submission.js
================================================
'use strict';
/**
* lead-form-submission router.
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::lead-form-submission.lead-form-submission');
================================================
FILE: packages/templates/corporate/template/src/api/lead-form-submission/services/lead-form-submission.js
================================================
'use strict';
/**
* lead-form-submission service.
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::lead-form-submission.lead-form-submission');
================================================
FILE: packages/templates/corporate/template/src/api/page/content-types/page/schema.json
================================================
{
"kind": "collectionType",
"collectionName": "pages",
"info": {
"singularName": "page",
"pluralName": "pages",
"displayName": "Page",
"name": "page"
},
"options": {
"increments": true,
"timestamps": true,
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"shortName": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"metadata": {
"type": "component",
"repeatable": false,
"component": "meta.metadata",
"required": true,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"contentSections": {
"type": "dynamiczone",
"components": [
"sections.hero",
"sections.bottom-actions",
"sections.feature-columns-group",
"sections.feature-rows-group",
"sections.testimonials-group",
"sections.large-video",
"sections.rich-text",
"sections.pricing",
"sections.lead-form"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"slug": {
"pluginOptions": {
"i18n": {
"localized": false
}
},
"type": "string",
"regex": "^$|^[a-zA-Z/-]+$"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/api/page/controllers/page.js
================================================
'use strict';
/**
* page controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::page.page');
================================================
FILE: packages/templates/corporate/template/src/api/page/routes/page.js
================================================
'use strict';
/**
* page router.
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::page.page');
================================================
FILE: packages/templates/corporate/template/src/api/page/services/page.js
================================================
'use strict';
/**
* page service.
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::page.page');
================================================
FILE: packages/templates/corporate/template/src/bootstrap.js
================================================
const fs = require("fs");
const { pages, globals, leadFormSubmissions } = require("../data/data");
const set = require("lodash.set");
async function isFirstRun() {
const pluginStore = strapi.store({
environment: strapi.config.environment,
type: "type",
name: "setup",
});
const initHasRun = await pluginStore.get({ key: "initHasRun" });
await pluginStore.set({ key: "initHasRun", value: true });
return !initHasRun;
}
async function setPublicPermissions(newPermissions) {
// Find the ID of the public role
const publicRole = await strapi
.query("plugin::users-permissions.role")
.findOne({
where: {
type: "public",
},
});
// Create the new permissions and link them to the public role
const allPermissionsToCreate = [];
Object.keys(newPermissions).map(controller => {
const actions = newPermissions[controller];
const permissionsToCreate = actions.map(action => {
return strapi.query("plugin::users-permissions.permission").create({
data: {
action: `api::${controller}.${controller}.${action}`,
role: publicRole.id,
},
});
});
allPermissionsToCreate.push(...permissionsToCreate);
});
await Promise.all(allPermissionsToCreate);
}
function getFileSizeInBytes(filePath) {
const stats = fs.statSync(filePath);
const fileSizeInBytes = stats["size"];
return fileSizeInBytes;
}
function getFileData(fileName) {
const filePath = `./data/uploads/${fileName}`;
// Parse the file metadata
const size = getFileSizeInBytes(filePath);
const ext = fileName.split(".").pop();
const mimeType = `image/${ext === "svg" ? "svg+xml" : ext}`;
return {
path: filePath,
name: fileName,
size,
type: mimeType,
};
}
// Create an entry and attach files if there are any
async function createEntry(model, entry, files) {
try {
if (files) {
for (const [key, file] of Object.entries(files)) {
// Get file name without the extension
const [fileName] = file.name.split('.');
// Upload each individual file
const uploadedFile = await strapi
.plugin("upload")
.service("upload")
.upload({
files: file,
data: {
fileInfo: {
alternativeText: fileName,
caption: fileName,
name: fileName,
},
},
});
// Attach each file to its entry
set(entry, key, uploadedFile[0].id);
}
}
// Actually create the entry in Strapi
const createdEntry = await strapi.entityService.create(
`api::${model}.${model}`,
{
data: entry,
}
);
} catch (e) {
console.log(e);
}
}
async function importPages(pages) {
const getPageCover = (slug) => {
switch (slug) {
case "":
return getFileData("undraw-content-team.png");
default:
return null;
}
};
return pages.map(async (page) => {
const files = {};
const shareImage = getPageCover(page.slug);
if (shareImage) {
files["metadata.shareImage"] = shareImage;
}
// Check if dynamic zone has attached files
page.contentSections.forEach((section, index) => {
if (section.__component === "sections.hero") {
files[`contentSections.${index}.picture`] = getFileData(
"undraw-content-team.svg"
);
} else if (section.__component === "sections.feature-rows-group") {
const getFeatureMedia = (featureIndex) => {
switch (featureIndex) {
case 0:
return getFileData("undraw-design-page.svg");
case 1:
return getFileData("undraw-create-page.svg");
default:
return null;
}
};
section.features.forEach((feature, featureIndex) => {
files[`contentSections.${index}.features.${featureIndex}.media`] =
getFeatureMedia(featureIndex);
});
} else if (section.__component === "sections.feature-columns-group") {
const getFeatureMedia = (featureIndex) => {
switch (featureIndex) {
case 0:
return getFileData("preview.svg");
case 1:
return getFileData("devices.svg");
case 2:
return getFileData("palette.svg");
default:
return null;
}
};
section.features.forEach((feature, featureIndex) => {
files[`contentSections.${index}.features.${featureIndex}.icon`] =
getFeatureMedia(featureIndex);
});
} else if (section.__component === "sections.testimonials-group") {
section.logos.forEach((logo, logoIndex) => {
files[`contentSections.${index}.logos.${logoIndex}.logo`] =
getFileData("logo.png");
});
section.testimonials.forEach((testimonial, testimonialIndex) => {
files[
`contentSections.${index}.testimonials.${testimonialIndex}.logo`
] = getFileData("logo.png");
files[
`contentSections.${index}.testimonials.${testimonialIndex}.picture`
] = getFileData("user.png");
});
}
});
await createEntry("page", page, files);
});
}
async function importGlobal() {
// Add images
const files = {
favicon: getFileData("favicon.png"),
"metadata.shareImage": getFileData("undraw-content-team.png"),
"navbar.logo": getFileData("logo.png"),
"footer.logo": getFileData("logo.png"),
};
// Create entry
globals.forEach(async (locale) => {
await createEntry("global", locale, files);
});
}
async function importLeadFormSubmissionData() {
leadFormSubmissions.forEach(async (submission) => {
await createEntry("lead-form-submissions", submission);
});
}
async function importSeedData() {
// Allow read of application content types
await setPublicPermissions({
global: ["find"],
page: ["find", "findOne"],
"lead-form-submission": ["create"],
});
await strapi.query("plugin::i18n.locale").create({
data: {
name: "French (fr)",
code: "fr",
},
});
// Create all entries
await importGlobal();
await importPages(pages);
await importLeadFormSubmissionData();
}
module.exports = async () => {
const shouldImportSeedData = await isFirstRun();
if (shouldImportSeedData) {
try {
await importSeedData();
} catch (error) {
console.log("Could not import seed data");
console.error(error);
}
}
};
================================================
FILE: packages/templates/corporate/template/src/components/elements/feature-column.json
================================================
{
"collectionName": "components_slices_feature_columns",
"info": {
"name": "FeatureColumn",
"displayName": "Feature column",
"icon": "align-center",
"description": ""
},
"options": {},
"attributes": {
"title": {
"type": "string",
"required": true
},
"description": {
"type": "text"
},
"icon": {
"allowedTypes": [
"images"
],
"type": "media",
"multiple": false,
"required": true
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/elements/feature-row.json
================================================
{
"collectionName": "components_slices_feature_rows",
"info": {
"name": "FeatureRow",
"displayName": "Feature row",
"icon": "arrows-alt-h",
"description": ""
},
"options": {},
"attributes": {
"title": {
"type": "string",
"required": true
},
"description": {
"type": "text"
},
"media": {
"allowedTypes": [
"images",
"videos"
],
"type": "media",
"multiple": false,
"required": true
},
"link": {
"type": "component",
"repeatable": false,
"component": "links.link"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/elements/feature.json
================================================
{
"collectionName": "components_elements_features",
"info": {
"name": "feature",
"displayName": "Feature",
"icon": "traffic-light"
},
"options": {},
"attributes": {
"name": {
"type": "string"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/elements/footer-section.json
================================================
{
"collectionName": "components_links_footer_sections",
"info": {
"name": "FooterSection",
"displayName": "Footer section",
"icon": "chevron-circle-down"
},
"options": {},
"attributes": {
"title": {
"type": "string"
},
"links": {
"type": "component",
"repeatable": true,
"component": "links.link"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/elements/logos.json
================================================
{
"collectionName": "components_elements_logos",
"info": {
"name": "logos",
"displayName": "Logos",
"icon": "apple-alt"
},
"options": {},
"attributes": {
"title": {
"type": "string"
},
"logo": {
"allowedTypes": [
"images"
],
"type": "media",
"multiple": false,
"required": false
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/elements/notification-banner.json
================================================
{
"collectionName": "components_elements_notification_banners",
"info": {
"name": "NotificationBanner",
"displayName": "Notification banner",
"icon": "exclamation"
},
"options": {},
"attributes": {
"text": {
"type": "richtext"
},
"type": {
"type": "enumeration",
"enum": [
"alert",
"info",
"warning"
],
"required": true
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/elements/plan.json
================================================
{
"collectionName": "components_elements_plans",
"info": {
"name": "plan",
"displayName": "Pricing plan",
"icon": "search-dollar"
},
"options": {},
"attributes": {
"name": {
"type": "string"
},
"description": {
"type": "text"
},
"features": {
"type": "component",
"repeatable": true,
"component": "elements.feature"
},
"isRecommended": {
"type": "boolean"
},
"price": {
"type": "decimal"
},
"pricePeriod": {
"type": "string"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/elements/testimonial.json
================================================
{
"collectionName": "components_slices_testimonials",
"info": {
"name": "Testimonial",
"displayName": "Testimonial",
"icon": "user-check"
},
"options": {},
"attributes": {
"logo": {
"allowedTypes": [
"images"
],
"type": "media",
"multiple": false,
"required": false
},
"picture": {
"allowedTypes": [
"images"
],
"type": "media",
"multiple": false,
"required": false
},
"text": {
"type": "text"
},
"authorName": {
"type": "string"
},
"authorTitle": {
"type": "string"
},
"link": {
"type": "string"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/layout/footer.json
================================================
{
"collectionName": "components_layout_footers",
"info": {
"name": "Footer",
"displayName": "Footer",
"icon": "caret-square-down"
},
"options": {},
"attributes": {
"logo": {
"allowedTypes": [
"images"
],
"type": "media",
"multiple": false,
"required": false
},
"columns": {
"type": "component",
"repeatable": true,
"component": "elements.footer-section"
},
"smallText": {
"type": "string"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/layout/navbar.json
================================================
{
"collectionName": "components_layout_navbars",
"info": {
"name": "Navbar",
"displayName": "Navbar",
"icon": "map-signs",
"description": ""
},
"options": {},
"attributes": {
"links": {
"type": "component",
"repeatable": true,
"component": "links.link"
},
"button": {
"type": "component",
"repeatable": false,
"component": "links.button-link"
},
"logo": {
"allowedTypes": [
"images"
],
"type": "media",
"multiple": false,
"required": true
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/links/button-link.json
================================================
{
"collectionName": "components_links_buttons",
"info": {
"name": "Button-link",
"displayName": "Button link",
"icon": "fingerprint",
"description": ""
},
"options": {},
"attributes": {
"url": {
"type": "string"
},
"newTab": {
"type": "boolean",
"default": false
},
"text": {
"type": "string"
},
"type": {
"type": "enumeration",
"enum": [
"primary",
"secondary"
]
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/links/button.json
================================================
{
"collectionName": "components_links_simple_buttons",
"info": {
"name": "Button",
"displayName": "Button",
"icon": "fingerprint",
"description": ""
},
"options": {},
"attributes": {
"text": {
"type": "string"
},
"type": {
"type": "enumeration",
"enum": [
"primary",
"secondary"
]
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/links/link.json
================================================
{
"collectionName": "components_links_links",
"info": {
"name": "Link",
"displayName": "Link",
"icon": "link",
"description": ""
},
"options": {},
"attributes": {
"url": {
"type": "string",
"required": true
},
"newTab": {
"type": "boolean",
"default": false
},
"text": {
"type": "string",
"required": true
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/meta/metadata.json
================================================
{
"collectionName": "components_meta_metadata",
"info": {
"name": "Metadata",
"displayName": "Metadata",
"icon": "robot"
},
"options": {},
"attributes": {
"metaTitle": {
"type": "string",
"required": true
},
"metaDescription": {
"type": "text",
"required": true
},
"shareImage": {
"allowedTypes": [
"images"
],
"type": "media",
"multiple": false,
"required": false
},
"twitterCardType": {
"type": "enumeration",
"enum": [
"summary",
"summary_large_image",
"app",
"player"
],
"default": "summary"
},
"twitterUsername": {
"type": "string"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/sections/bottom-actions.json
================================================
{
"collectionName": "components_slices_bottom_actions",
"info": {
"name": "BottomActions",
"displayName": "Bottom actions",
"icon": "angle-double-right"
},
"options": {},
"attributes": {
"title": {
"type": "string"
},
"buttons": {
"type": "component",
"repeatable": true,
"component": "links.button-link"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/sections/feature-columns-group.json
================================================
{
"collectionName": "components_slices_feature_columns_groups",
"info": {
"name": "FeatureColumnsGroup",
"displayName": "Feature columns group",
"icon": "star-of-life"
},
"options": {},
"attributes": {
"features": {
"type": "component",
"repeatable": true,
"component": "elements.feature-column"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/sections/feature-rows-group.json
================================================
{
"collectionName": "components_slices_feature_rows_groups",
"info": {
"name": "FeatureRowsGroup",
"displayName": "Feaures row group",
"icon": "bars"
},
"options": {},
"attributes": {
"features": {
"type": "component",
"repeatable": true,
"component": "elements.feature-row"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/sections/hero.json
================================================
{
"collectionName": "components_slices_heroes",
"info": {
"name": "Hero",
"displayName": "Hero",
"icon": "heading"
},
"options": {},
"attributes": {
"title": {
"type": "string"
},
"label": {
"type": "string"
},
"description": {
"type": "string"
},
"picture": {
"allowedTypes": [
"images"
],
"type": "media",
"multiple": false,
"required": false
},
"smallTextWithLink": {
"type": "richtext"
},
"buttons": {
"type": "component",
"repeatable": true,
"component": "links.button-link"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/sections/large-video.json
================================================
{
"collectionName": "components_slices_large_videos",
"info": {
"name": "LargeVideo",
"displayName": "Large video",
"icon": "play-circle"
},
"options": {},
"attributes": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"video": {
"allowedTypes": [
"videos"
],
"type": "media",
"multiple": false,
"required": true
},
"poster": {
"allowedTypes": [
"images"
],
"type": "media",
"multiple": false,
"required": false
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/sections/lead-form.json
================================================
{
"collectionName": "components_sections_lead_forms",
"info": {
"name": "Lead form",
"displayName": "Lead form",
"icon": "at",
"description": ""
},
"options": {},
"attributes": {
"title": {
"type": "string"
},
"emailPlaceholder": {
"type": "string"
},
"submitButton": {
"type": "component",
"repeatable": false,
"component": "links.button"
},
"location": {
"type": "string"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/sections/pricing.json
================================================
{
"collectionName": "components_sections_pricings",
"info": {
"name": "Pricing",
"displayName": "Pricing",
"icon": "dollar-sign"
},
"options": {},
"attributes": {
"title": {
"type": "string"
},
"plans": {
"type": "component",
"repeatable": true,
"component": "elements.plan"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/sections/rich-text.json
================================================
{
"collectionName": "components_sections_rich_texts",
"info": {
"name": "RichText",
"displayName": "Rich text",
"icon": "text-height"
},
"options": {},
"attributes": {
"content": {
"type": "richtext"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/components/sections/testimonials-group.json
================================================
{
"collectionName": "components_slices_testimonials_groups",
"info": {
"name": "TestimonialsGroup",
"displayName": "Testimonials group",
"icon": "user-friends"
},
"options": {},
"attributes": {
"title": {
"type": "string"
},
"description": {
"type": "text"
},
"link": {
"type": "component",
"repeatable": false,
"component": "links.link"
},
"logos": {
"type": "component",
"repeatable": true,
"component": "elements.logos"
},
"testimonials": {
"type": "component",
"repeatable": true,
"component": "elements.testimonial"
}
}
}
================================================
FILE: packages/templates/corporate/template/src/index.js
================================================
"use strict";
const boostrap = require('./bootstrap');
module.exports = {
async bootstrap() {
await boostrap();
},
}
================================================
FILE: packages/templates/corporate/template.json
================================================
{
"package": {
"dependencies": {
"@strapi/plugin-graphql": "^4.0.0",
"lodash.set": "^4.3.2"
}
}
}
================================================
FILE: packages/templates/ecommerce/README.md
================================================
# strapi-template-ecommerce
A Strapi template to create Strapi projects pre-configured for e-commerce apps.
## Usage
```bash
# Using Yarn
yarn create strapi-app my-app-name --template ecommerce
# Or using NPM
npx create-strapi-app my-app-name --template ecommerce
```
## Starters
This template is used by the following starters:
* [Strapi Starter Nuxt.js E-commerce](https://github.com/strapi/strapi-starter-nuxt-e-commerce)
* [Strapi Starter Next.js E-commerce](https://github.com/strapi/strapi-starter-next-ecommerce)
================================================
FILE: packages/templates/ecommerce/package.json
================================================
{
"name": "@strapi/template-ecommerce",
"version": "1.0.0",
"description": "Strapi ecommerce template",
"keywords": [
"strapi",
"template",
"ecommerce"
],
"homepage": "https://github.com/strapi/starters-and-templates#readme",
"bugs": {
"url": "https://github.com/strapi/starters-and-templates/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/strapi/starters-and-templates.git"
},
"license": "MIT",
"author": {
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
},
"maintainers": [
{
"name": "Strapi team",
"email": "hi@strapi.io",
"url": "https://strapi.io"
}
],
"gitHead": "d6d5bd264d1df0a0f23b5234d84348f8d78882e3"
}
================================================
FILE: packages/templates/ecommerce/template/data/data.js
================================================
module.exports = {
categories: [
{
name: "Back",
slug: "back",
},
{
name: "Front",
slug: "front",
},
{
name: "SSG",
slug: "ssg",
},
{
name: "Container",
slug: "container",
},
{
name: "Database",
slug: "database",
},
{
name: "Other",
slug: "other",
},
],
products: [
{
title: "Gatsby",
description:
"Blazing fast modern site generator for React. Go beyond static sites: build blogs, e-commerce sites, full-blown apps, and more with Gatsby.",
price: 1.84,
slug: "gatsby",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 3,
},
],
},
{
title: "Nuxt.js",
description: "The Intuitive Vue Framework",
price: 1.84,
slug: "nuxt-js",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 3,
},
{
id: 2,
},
],
},
{
title: "Vue.js",
description: "The Progressive JavaScript Framework.",
price: 1.84,
slug: "vue-js",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 2,
},
],
},
{
title: "Kubernetes",
description: "Production-Grade Container Orchestration",
price: 1.84,
slug: "kubernetes",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 4,
},
],
},
{
title: "Docker",
description: "Empowering App Development for Developers",
price: 1.84,
slug: "docker",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 4,
},
],
},
{
title: "Next.js",
description: "The React Framework",
price: 1.84,
slug: "next-js",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 3,
},
{
id: 2,
},
],
},
{
title: "npm",
description: "Build amazing things",
price: 1.84,
slug: "npm",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 6,
},
],
},
{
title: "Swift",
description: "Apple Developer",
price: 1.84,
slug: "swift",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 2,
},
{
id: 1,
},
],
},
{
title: "Rust",
description:
"A language empowering everyone to build reliable and efficient software.",
price: 1.84,
slug: "rust",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 1,
},
],
},
{
title: "SQLite",
description: "Small. Fast. Reliable. Choose any three",
price: 1.84,
slug: "sq-lite",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 5,
},
],
},
{
title: "PostgreSQL",
description: "The world's most advanced open source database",
price: 1.84,
slug: "postgre-sql",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 5,
},
],
},
{
title: "Ruby on Rails",
description:
"A web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller ",
price: 1.84,
slug: "ruby-on-rails",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 2,
},
{
id: 1,
},
],
},
{
title: "MongoDB",
description: "The most popular database for modern apps",
price: 1.84,
slug: "mongo-db",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 5,
},
],
},
{
title: "Laravel",
description: "The PHP Framework For Web Artisans",
price: 1.84,
slug: "laravel",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 2,
},
{
id: 1,
},
],
},
{
title: "GraphQL",
description: "A query language for your API",
price: 1.84,
slug: "graph-ql",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 6,
},
],
},
{
title: "Golang",
description:
"Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.",
price: 1.84,
slug: "golang",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 1,
},
],
},
{
title: "Google cloud",
description: "Cloud Computing Services",
price: 1.84,
slug: "google-cloud",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 6,
},
],
},
{
title: "C++",
description:
'C++ is a general-purpose programming language created by Bjarne Stroustrup as an extension of the C programming language, or "C with Classes"',
price: 1.84,
slug: "c-plus-plus",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 1,
},
],
},
{
title: "Kotlin",
description:
"A modern programming language that makes developers happier. Open source forever",
price: 1.84,
slug: "kotlin",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 1,
},
{
id: 2,
},
],
},
{
title: "React",
description: "A JavaScript library for building user interfaces",
price: 1.84,
slug: "react",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 2,
},
],
},
{
title: "Ruby",
description:
"A dynamic, open source programming language with a focus on simplicity and productivity. It has an elegant syntax that is natural to read and easy to write.",
price: 1.84,
slug: "ruby-1",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 1,
},
],
},
{
title: "Python",
description:
"Python is a programming language that lets you work quickly and integrate systems more effectively.",
price: 1.84,
slug: "python",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 1,
},
],
},
{
title: "PHP",
description: "Hypertext Preprocessor",
price: 1.84,
slug: "php",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 1,
},
],
},
{
title: "Linux",
description:
"Linux is a family of open source Unix-like operating systems based on the Linux kernel, an operating system kernel first released on September 17, 1991, by Linus Torvalds.",
price: 1.84,
slug: "linux",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 6,
},
],
},
{
title: "Java",
description:
"Java is a general-purpose programming language that is class-based, object-oriented, and designed to have as few implementation dependencies as possible.",
price: 1.84,
slug: "java",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 1,
},
],
},
{
title: "Angular",
description: "One framework. Mobile & desktop.",
price: 1.84,
slug: "angular",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 2,
},
],
},
{
title: "Strapi",
description: "Design APIs fast, manage content easily.",
price: 1.84,
slug: "strapi",
status: "published",
Custom_field: [
{
title: "Select the size of your sticker",
required: true,
options: "Small[+0.00]|Medium[+0.40]|Large[+0.80]",
},
],
image: null,
categories: [
{
id: 1,
},
],
},
],
};
================================================
FILE: packages/templates/ecommerce/template/src/api/.gitkeep
================================================
================================================
FILE: packages/templates/ecommerce/template/src/api/category/content-types/category/schema.json
================================================
{
"kind": "collectionType",
"collectionName": "categories",
"info": {
"singularName": "category",
"pluralName": "categories",
"displayName": "Category",
"name": "category"
},
"options": {
"increments": true,
"timestamps": true
},
"attributes": {
"name": {
"type": "string"
},
"slug": {
"type": "uid",
"targetField": "name"
},
"products": {
"type": "relation",
"relation": "manyToMany",
"target": "api::product.product",
"inversedBy": "categories"
}
}
}
================================================
FILE: packages/templates/ecommerce/template/src/api/category/controllers/category.js
================================================
'use strict';
/**
* category controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::category.category');
================================================
FILE: packages/templates/ecommerce/template/src/api/category/routes/category.js
================================================
'use strict';
/**
* category router.
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::category.category');
================================================
FILE: packages/templates/ecommerce/template/src/api/category/services/category.js
================================================
'use strict';
/**
* category service.
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::category.category');
================================================
FILE: packages/templates/ecommerce/template/src/api/product/content-types/product/schema.json
================================================
{
"kind": "collectionType",
"collectionName": "products",
"info": {
"singularName": "product",
"pluralName": "products",
"displayName": "Product",
"name": "product"
},
"options": {
"increments": true,
"timestamps": true
},
"attributes": {
"title": {
"type": "string",
"required": true
},
"description": {
"type": "string",
"required": true
},
"price": {
"type": "float"
},
"image": {
"allowedTypes": [
"images"
],
"type": "media",
"multiple": false
},
"slug": {
"type": "uid",
"targetField": "title"
},
"categories": {
"type": "relation",
"relation": "manyToMany",
"target": "api::category.category",
"inversedBy": "products"
},
"Custom_field": {
"type": "component",
"repeatable": true,
"component": "custom.custom-field"
},
"status": {
"type": "enumeration",
"enum": [
"draft",
"published"
],
"default": "published",
"required": true
}
}
}
================================================
FILE: packages/templates/ecommerce/template/src/api/product/controllers/product.js
================================================
'use strict';
/**
* product controller
*/
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::product.product');
================================================
FILE: packages/templates/ecommerce/template/src/api/product/routes/product.js
================================================
'use strict';
/**
* product router.
*/
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::product.product');
================================================
FILE: packages/templates/ecommerce/template/src/api/product/services/product.js
================================================
'use strict';
/**
* product service.
*/
const { createCoreService } = require('@strapi/strapi').factories;
module.exports = createCoreService('api::product.product');
================================================
FILE: packages/templates/ecommerce/template/src/bootstrap.js
================================================
"use strict";
const fs = require("fs");
const path = require("path");
const mime = require("mime-types");
const set = require("lodash.set");
const { categories, products } = require("../data/data");
async function setPublicPermissions(newPermissions) {
// Find the ID of the public role
const publicRole = await strapi
.query("plugin::users-permissions.role")
.findOne({
where: {
type: "public",
},
});
// Create the new permissions and link them to the public role
const allPermissionsToCreate = [];
Object.keys(newPermissions).map((controller) => {
const actions = newPermissions[controller];
const permissionsToCreate = actions.map((action) => {
return strapi.query("plugin::users-permissions.permission").create({
data: {
action: `api::${controller}.${controller}.${action}`,
role: publicRole.id,
},
});
});
allPermissionsToCreate.push(...permissionsToCreate);
});
await Promise.all(allPermissionsToCreate);
}
async function isFirstRun() {
const pluginStore = strapi.store({
environment: strapi.config.environment,
type: "type",
name: "setup",
});
const initHasRun = await pluginStore.get({ key: "initHasRun" });
await pluginStore.set({ key: "initHasRun", value: true });
return !initHasRun;
}
function getFileSizeInBytes(filePath) {
const stats = fs.statSync(filePath);
const fileSizeInBytes = stats["size"];
return fileSizeInBytes;
}
function getFileData(fileName) {
const filePath = `./data/uploads/${fileName}`;
// Parse the file metadata
const size = getFileSizeInBytes(filePath);
const ext = fileName.split(".").pop();
const mimeType = mime.lookup(ext);
return {
path: filePath,
name: fileName,
size,
type: mimeType,
};
}
// Create an entry and attach files if there are any
async function createEntry({ model, entry, files }) {
try {
if (files) {
for (const [key, file] of Object.entries(files)) {
// Get file name without the extension
const [fileName] = file.name.split(".");
// Upload each individual file
const uploadedFile = await strapi
.plugin("upload")
.service("upload")
.upload({
files: file,
data: {
fileInfo: {
alternativeText: fileName,
caption: fileName,
name: fileName,
},
},
});
// Attach each file to its entry
set(entry, key, uploadedFile[0].id);
}
}
// Actually create the entry in Strapi
const createdEntry = await strapi.entityService.create(
`api::${model}.${model}`,
{
data: entry,
}
);
} catch (e) {
console.log("model", entry, e);
}
}
async function importCategories() {
return Promise.all(
categories.map((category) => {
return createEntry({ model: "category", entry: category });
})
);
}
async function importProducts() {
return Promise.all(
products.map(async (product) => {
const files = {
image: getFileData(`${product.slug}.png`),
};
return createEntry({
model: "product",
entry: product,
files,
});
})
);
}
async function importSeedData() {
// Allow read of application content types
await setPublicPermissions({
category: ["find", "findOne"],
product: ["find", "findOne"],
});
// Create all entries
await importCategories(categories);
await importProducts(products);
}
module.exports = async () => {
const shouldImportSeedData = await isFirstRun();
if (shouldImportSeedData) {
try {
await importSeedData();
} catch (error) {
console.log("Could not import seed data");
console.error(error);
}
}
};
================================================
FILE: packages/templates/ecommerce/template/src/components/custom/custom-field.json
================================================
{
"collectionName": "components_custom_custom_fields",
"info": {
"name": "Custom_field",
"displayName": "Custom field",
"icon": "archway"
},
"options": {},
"attributes": {
"title": {
"type": "string"
},
"required": {
"type": "boolean"
},
"options": {
"type": "string"
}
}
}
================================================
FILE: packages/templates/ecommerce/template/src/index.js
================================================
"use strict";
const bootstrap = require('./bootstrap');
module.exports = {
async bootstrap() {
await bootstrap();
},
}
================================================
FILE: packages/templates/ecommerce/template.json
================================================
{
"package": {
"dependencies": {
"@strapi/plugin-graphql": "^4.0.0",
"lodash.set": "^4.3.2",
"mime-types": "^2.1.27"
}
}
}