Skip to content

Commit f462919

Browse files
committed
Add documentation example
1 parent 7e25e47 commit f462919

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+26746
-0
lines changed

documentation/.env.local-sample

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CONTENTFUL_SPACE_ID=""
2+
CONTENTFUL_PREVIEW_TOKEN=""
3+
CONTENTFUL_DELIVERY_TOKEN=""
4+
CONTENTFUL_ACCESS_TOKEN=""

documentation/.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "next/core-web-vitals"
3+
}

documentation/.gitignore

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
.pnpm-debug.log*
27+
28+
# local env files
29+
.env*
30+
!.env.local-sample
31+
32+
# vercel
33+
.vercel
34+
35+
# typescript
36+
*.tsbuildinfo
37+
next-env.d.ts
38+
39+
# stackbit
40+
.stackbit/cache/

documentation/.prettierrc

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"tabWidth": 4,
3+
"singleQuote": true,
4+
"trailingComma": "none",
5+
"printWidth": 160
6+
}

documentation/README.md

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Documentation Site Example
2+
3+
This is an example of a documentation site built with:
4+
5+
- [Next.js](https://nextjs.org/) (site framework)
6+
- [Contentful](https://www.contentful.com/) (content source)
7+
- [Stackbit](https://www.stackbit.com/) (visual editor)
8+
- [Tailwind](https://tailwindcss.com/) (CSS framework)
9+
10+
This site is an example of a site that uses a headless CMS (Contentful) to manage the content. Among other benefits, it is easier to account for multiple content editors of varying technical skill levels.
11+
12+
The content modeling here is inspired by [Notion](https://www.notion.so/), which does an excellent job of balancing flexibility and productivity for content editors.
13+
14+
### Features
15+
16+
While this is only a simple example to get started, we've introduced a few features that are worth noting:
17+
18+
- **Theme Toggle:** A simple switch, along with styles to support both light and dark modes.
19+
- **Syntax Highlighting:** The `<CodeBlock />` component uses [highlight.js](https://highlightjs.org/) for syntax highlighting.
20+
- **Composable Pages:** Every page is just a page and can be composed of any number of sections. This allows for a lot of flexibility in how content is organized.
21+
- **Stackbit-Ready:** Stackbit helps you build sites faster by providing a visual editor for content. This site works with Stackbit out of the box.
22+
23+
## Getting Started
24+
25+
Getting started with this project just takes a few steps.
26+
27+
### Clone the Repository
28+
29+
This is an official example, so you can use `create-stackbit-app` to clone it to your machine:
30+
31+
npx create-stackbit-app@latest --example documentation stackbit-docs-example
32+
33+
This will create a new project in a `stackbit-docs-example` directory.
34+
35+
### Setup Contentful
36+
37+
This project uses Contentful as a headless CMS. You'll need to do the following to get Contentful ready to go:
38+
39+
1. Create a Contentful account (if you don't have one).
40+
1. Create a new space. This will become the `CONTENTFUL_SPACE_ID` environment variable.
41+
1. Create a new [personal access token](https://www.contentful.com/help/personal-access-tokens/). This will become the `CONTENTFUL_ACCESS_TOKEN` environment variable.
42+
1. [Add an API key](https://training.contentful.com/student/page/1050378-api-keys) to the space. You will need the delivery and preview tokens, which will become the `CONTENTFUL_DELIVERY_TOKEN` and `CONTENTFUL_PREVIEW_TOKEN` environment variables.
43+
44+
Copy `.env.local-sample` to `.env.local` and fill in the environment variables mentioned above.
45+
46+
### Import Content
47+
48+
There is a script ready to import the content model and content into your Contentful space. You can run it with the following command:
49+
50+
npm run contentful:import -- <CONTENTFUL_ACCESS_TOKEN> <CONTENTFUL_SPACE_ID>
51+
52+
Replace `<CONTENTFUL_ACCESS_TOKEN>` and `<CONTENTFUL_SPACE_ID>` with the values from your Contentful space.
53+
54+
### Run the Server
55+
56+
Now you should be able to run the server:
57+
58+
npm run dev
59+
60+
You can also run Stackbit's development server in a parallel process. This will allow you to edit content in Stackbit's visual editor locally on your machine.
61+
62+
npm install -g @stackbit/cli
63+
stackbit dev
64+
65+
This will output a URL that you can open to view the visual editor.
66+
67+
## Adding a Component
68+
69+
If you want to add a component that can be used in a page, you'll need to do the following:
70+
71+
1. Add the new content type in Contentful with the appropriate fields.
72+
1. Edit the `sections` field in the page model to allow for your new component.
73+
1. Export the content from your space into this project.
74+
75+
```txt
76+
npm run contentful:export
77+
```
78+
79+
1. Generate types for the new content type.
80+
81+
```txt
82+
npm run contentful:types
83+
```
84+
85+
1. Add new, custom type(s) to `types/index.ts`, which requires:
86+
1. Adding any new sub-components to the _Atoms_ section.
87+
1. Add new content type ID to `SectionType` type.
88+
1. Add a new type based on the generic `Section` type and the automatically-generated type for the new content type.
89+
1. Add new type to the `ComposableSection` type.
90+
1. Add a new component in the `components` directory. Use the new type from `types/index.ts` as the type for the component's props.
91+
1. Add the new component to `components/DynamicComponent.tsx`.

documentation/components/Badge.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Badge as BadgeProps } from '@/types';
2+
3+
export const Badge: React.FC<BadgeProps> = (props) => {
4+
return (
5+
<span
6+
className="inline-block border leading-none px-2 py-[.125rem] rounded-full text-sm text-indigo-800 bg-indigo-100 border-indigo-200 dark:bg-indigo-700 dark:text-slate-100 dark:border-indigo-500"
7+
data-sb-object-id={props._id}
8+
>
9+
<span data-sb-field-path="title">{props.title}</span>
10+
</span>
11+
);
12+
};

documentation/components/Button.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Button as ButtonProps } from '@/types';
2+
import Link from 'next/link';
3+
import { Icon } from './Icon';
4+
5+
export const Button: React.FC<ButtonProps> = (props) => {
6+
return (
7+
<Link
8+
href={props.href}
9+
className="inline-flex items-center space-x-3 text-lg text-indigo-500 transition-all duration-300 dark:text-indigo-300 dark:hover:opacity-70 hover:text-indigo-700"
10+
data-sb-object-id={props._id}
11+
>
12+
<span data-sb-field-path="title">{props.title}</span>
13+
{props.showArrow && (
14+
<span className="block w-4">
15+
<Icon.ArrowRight />
16+
</span>
17+
)}
18+
</Link>
19+
);
20+
};

documentation/components/Callout.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Callout as CalloutProps } from '@/types';
2+
import { Icon } from './Icon';
3+
4+
export const Callout: React.FC<CalloutProps> = (props) => {
5+
return (
6+
<div
7+
className="flex items-start justify-start p-4 my-8 space-x-3 border rounded-md section bg-slate-100 dark:bg-slate-900 border-slate-200 dark:border-slate-600"
8+
data-sb-object-id={props._id}
9+
>
10+
<span className="relative flex-shrink-0 block w-6 text-slate-400">
11+
<Icon.Info />
12+
</span>
13+
<span className="block" dangerouslySetInnerHTML={{ __html: props.body }} data-sb-field-path="body" />
14+
</div>
15+
);
16+
};

documentation/components/Card.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Card as CardProps } from '@/types';
2+
import Link from 'next/link';
3+
4+
export const Card: React.FC<CardProps> = (props) => {
5+
return (
6+
<Link
7+
className="block p-5 transition-all duration-300 border rounded-md border-slate-200 dark:border-slate-600 hover:shadow-md dark:shadow-slate-600"
8+
href={props.href}
9+
data-sb-object-id={props._id}
10+
>
11+
<h3 className="mb-3 text-lg" data-sb-field-path="title">
12+
{props.title}
13+
</h3>
14+
<div dangerouslySetInnerHTML={{ __html: props.body }} className="text-sm" data-sb-field-path="body" />
15+
</Link>
16+
);
17+
};

documentation/components/CardGrid.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { CardGrid as CardGridProps } from '@/types';
2+
import { Card } from './Card';
3+
4+
export const CardGrid: React.FC<CardGridProps> = (props) => {
5+
return (
6+
<div className="grid grid-cols-3 gap-6 my-6" data-sb-object-id={props._id}>
7+
{props.cards.map((card, index) => {
8+
return <Card key={index} {...card} />;
9+
})}
10+
</div>
11+
);
12+
};
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { CodeBlock as CodeBlockProps } from '@/types';
2+
3+
export const CodeBlock: React.FC<CodeBlockProps> = (props) => {
4+
return (
5+
<div className="my-8 border rounded-md section bg-slate-50 border-slate-200 dark:bg-slate-900 dark:border-slate-600" data-sb-object-id={props._id}>
6+
{props.label && (
7+
<div className="px-4 py-1 border-b border-slate-200 bg-slate-200 dark:border-slate-600 dark:bg-slate-800">
8+
<span className="font-mono text-xs font-bold text-slate-500 dark:text-slate-200" data-sb-field-path="label">
9+
{props.label}
10+
</span>
11+
</div>
12+
)}
13+
<pre className="p-4 overflow-x-scroll" data-sb-field-path="body">
14+
<code className={`language-${props.code.language} bg-transparent border-none p-0`} dangerouslySetInnerHTML={{ __html: props.code.html }} />
15+
</pre>
16+
</div>
17+
);
18+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ComposableSection, SectionType } from '@/types';
2+
import dynamic from 'next/dynamic';
3+
import { ComponentType } from 'react';
4+
5+
const componentsMap: { [P in ComposableSection as P['_type']]: ComponentType<P> } = {
6+
callout: dynamic(() => import('./Callout').then((mod) => mod.Callout)),
7+
cardGrid: dynamic(() => import('./CardGrid').then((mod) => mod.CardGrid)),
8+
codeBlock: dynamic(() => import('./CodeBlock').then((mod) => mod.CodeBlock)),
9+
heading: dynamic(() => import('./Heading').then((mod) => mod.Heading)),
10+
hero: dynamic(() => import('./Hero').then((mod) => mod.Hero)),
11+
image: dynamic(() => import('./Image').then((mod) => mod.Image)),
12+
list: dynamic(() => import('./List').then((mod) => mod.List)),
13+
paragraph: dynamic(() => import('./Paragraph').then((mod) => mod.Paragraph))
14+
};
15+
16+
export const DynamicComponent: React.FC<ComposableSection> = (props) => {
17+
if (!props._type) {
18+
throw new Error(`Object does not have the '_type' property required to select a component: ${JSON.stringify(props, null, 2)}`);
19+
}
20+
const Component = componentsMap[props._type] as ComponentType<ComposableSection>;
21+
if (!Component) {
22+
throw new Error(
23+
`No component match object with type: '${props._type}'\nMake sure DynamicComponent.tsx file has an entry for '${props._type}' in 'componentsMap'`
24+
);
25+
}
26+
return <Component {...props} />;
27+
};

documentation/components/Footer.tsx

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Link from 'next/link';
2+
import { Icon } from './Icon';
3+
4+
export const Footer: React.FC = () => {
5+
return (
6+
<div className="py-8 mt-12 border-t border-slate-200 dark:border-slate-600 text-slate-600 dark:text-slate-300">
7+
<span className="flex items-center justify-center space-x-2 text-sm">
8+
<span>Built with</span>
9+
<span className="block w-4">
10+
<Icon.Heart />
11+
</span>
12+
<span>
13+
by{' '}
14+
<Link href="https://www.stackbit.com/" className="text-indigo-500 hover:text-indigo-800" target="_blank">
15+
Stackbit
16+
</Link>
17+
</span>
18+
</span>
19+
</div>
20+
);
21+
};

documentation/components/Header.tsx

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Theme } from '@/hooks/useThemeSwitcher';
2+
import { SiteConfig } from '@/types';
3+
import Link from 'next/link';
4+
import { Icon } from './Icon';
5+
6+
type Props = SiteConfig & {
7+
toggleTheme: Function;
8+
theme: Theme;
9+
};
10+
11+
export const Header: React.FC<Props> = (props) => {
12+
const IconTagName = props.theme === 'dark' ? Icon.Sun : Icon.Moon;
13+
14+
return (
15+
<div className="border-b border-slate-200 dark:border-slate-600" data-sb-object-id={props._id}>
16+
<div className="flex items-center justify-between px-6 py-4">
17+
<Link href="/" className="font-black" data-sb-field-path="title">
18+
{props.title}
19+
</Link>
20+
<div className="flex items-center space-x-4">
21+
<button className="w-5" onClick={() => props.toggleTheme()}>
22+
<IconTagName />
23+
</button>
24+
{props.githubUrl && (
25+
<a href={props.githubUrl} className="w-5 mr-5" target="_blank" rel="noreferrer" data-sb-field-path="githubUrl#@href">
26+
<Icon.GitHub />
27+
</a>
28+
)}
29+
</div>
30+
</div>
31+
</div>
32+
);
33+
};

documentation/components/Heading.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Heading as HeadingProps } from '@/types';
2+
import { Badge } from './Badge';
3+
4+
const headingTagMap: {
5+
[K in HeadingProps['level']]: { tagName: React.ElementType; className: string };
6+
} = {
7+
'1': { tagName: 'h1', className: 'text-3xl mb-3 mt-12' },
8+
'2': { tagName: 'h2', className: 'mb-3 mt-10' },
9+
'3': { tagName: 'h3', className: 'mb-1 mt-8' },
10+
'4': { tagName: 'h4', className: 'mb-1 mt-6' },
11+
'5': { tagName: 'h5', className: 'mb-1 mt-5' },
12+
'6': { tagName: 'h6', className: 'mb-1 mt-5' }
13+
};
14+
15+
export const Heading: React.FC<HeadingProps> = (props) => {
16+
const TagName = headingTagMap[props.level].tagName;
17+
return (
18+
<TagName className={`flex items-center space-x-3 ${headingTagMap[props.level].className}`} id={props._id} data-sb-object-id={props._id}>
19+
<span className="inline-block" data-sb-field-path="body">
20+
{props.body}
21+
</span>
22+
{props.badge?._id && <Badge {...props.badge} />}
23+
</TagName>
24+
);
25+
};

documentation/components/Hero.tsx

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Hero as HeroProps } from '@/types';
2+
import NextImage from 'next/image';
3+
import { Badge } from './Badge';
4+
import { Button } from './Button';
5+
6+
export const Hero: React.FC<HeroProps> = (props) => {
7+
let imageSrc = props.image.file.url;
8+
if (imageSrc.startsWith('//')) imageSrc = `https:${imageSrc}`;
9+
10+
return (
11+
<div className="mt-12 mb-20" data-sb-object-id={props._id}>
12+
<div className="flex space-x-12">
13+
<div>
14+
{props.badge && (
15+
<span className="block mb-3">
16+
<Badge {...props.badge} />
17+
</span>
18+
)}
19+
<h2 className="mb-4" data-sb-field-path="title">
20+
{props.title}
21+
</h2>
22+
<div dangerouslySetInnerHTML={{ __html: props.body }} className="mb-4" data-sb-field-path="body" />
23+
<Button {...props.button} />
24+
</div>
25+
<div className="flex-shrink-0 max-w-sm">
26+
<NextImage
27+
src={imageSrc}
28+
alt={props.title}
29+
width={props.image.file.details.image?.width}
30+
height={props.image.file.details.image?.height}
31+
className="overflow-hidden rounded-md"
32+
data-sb-field-path="image"
33+
/>
34+
</div>
35+
</div>
36+
</div>
37+
);
38+
};

0 commit comments

Comments
 (0)