Marketing Pages
The most requested feature is here! Level up your SaaS by adding a landing page, docs, blog and many other pages!
29 Nov 202410 minutes read
Summary
We've built a comprehensive set of marketing pages to cover every touchpoint of your SaaS:
- Landing – A beautiful page to showcase your product, optimized for clear value proposition and call-to-action.
- Blog – Integrated blogging system that is statically generated during build time.
- Docs – Integrated documentation pages that is statically generated during build time.
- Pricing – Responsive pricing tables with a plan comparisons and conversion-optimized design.
- Story – Authentic company narrative page that builds trust and connects with your audience.
- Legal – Templates for Terms of Service, Privacy Policy and Cookie Policy.
- Careers – Values and job listings page.
- Contact – Contact form.
Both the blog and docs are using content collections. That means new content can be added via MDX files.
Design Philosophy
The design follows the latest web trends: a clean grid system with guide lines and subtle hatching patterns.
Landing Page
Blog Pages
Docs
Pricing Page
Story Page
Legal Pages
...also privacy policy page and cookie policy page.
Careers Page
Contact Page
How to update
The good thing, given the size of the update, there are not many changes, just a lot of addtions.
Automatic Update
If you use the Git upstream repository, a simple git pull will already keep you up-to-date.
Manual update
Add following routes to @/constants/routes.ts
enum Routes {
// ...
Contact = '/contact',
Roadmap = 'https://achromatic.canny.io',
Docs = '/docs',
Pricing = '/pricing',
Blog = '/blog',
Story = '/story',
Careers = '/careers',
TermsOfUse = '/terms-of-use',
PrivacyPolicy = '/privacy-policy',
CookiePolicy = '/cookie-policy'
// ...
}
Add the following hook @/hooks/use-mounted.tsx
import * as React from 'react';
export function useMounted(): boolean {
const [mounted, setMounted] = React.useState<boolean>(false);
React.useEffect(() => {
setMounted(true);
}, []);
return mounted;
}
Add or change your package.json
// scripts
"build": "content-collections build && next build",
"build:content": "content-collections build",
"typecheck": "content-collections build && tsc --noEmit",
// dependencies
"@radix-ui/react-portal": "1.1.2",
"framer-motion": "11.11.17",
"mdast-util-toc": "7.1.0",
"react-remove-scroll": "2.6.0",
"unist-util-visit": "5.0.0"
// dev dependencies
"@content-collections/cli": "0.1.6",
"@content-collections/core": "0.7.3",
"@content-collections/mdx": "0.2.0",
"@content-collections/next": "0.2.4",
"@types/unist": "3.0.3",
"rehype": "13.0.2",
"rehype-autolink-headings": "7.1.0",
"rehype-pretty-code": "0.14.0",
"rehype-slug": "6.0.0",
"remark": "15.0.1",
"remark-code-import": "1.2.0",
"remark-gfm": "4.0.0",
"shiki": "1.23.1"
Add following path to tsconfig.json
"content-collections": ["./.content-collections/generated"]
Add following css helpers to your tailwind.config.cjs
backgroundImage: {
'diagonal-lines':
'repeating-linear-gradient(-45deg, hsl(var(--background)), hsl(var(--border)) 1px, hsl(var(--background)) 1px, hsl(var(--background)) 8px)'
},
keyFrames: {
marquee: {
from: { transform: 'translateX(0)' },
to: { transform: 'translateX(calc(-100% - var(--gap)))' }
},
'marquee-vertical': {
from: { transform: 'translateY(0)' },
to: { transform: 'translateY(calc(-100% - var(--gap)))' }
}
},
animation: {
marquee: 'marquee var(--duration) linear infinite',
'marquee-vertical': 'marquee-vertical var(--duration) linear infinite'
}
Now in your next-config.mjs
remove the standard redirect:
{
source: '/',
destination: '/dashboard/home',
permanent: false
}
Then add content collections to the same file
import { withContentCollections } from '@content-collections/next';
export default withContentCollections(bundleAnalyzerConfig(nextConfig));
Optionally you can also add the remote patterns for the example avatars
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'randomuser.me',
port: '',
pathname: '**',
search: ''
}
]
},
Now add the file content-collections.ts
at the root level
import path from 'path';
import { defineCollection, defineConfig } from '@content-collections/core';
import { compileMDX } from '@content-collections/mdx';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypePrettyCode, { Options } from 'rehype-pretty-code';
import rehypeSlug from 'rehype-slug';
import { codeImport } from 'remark-code-import';
import remarkGfm from 'remark-gfm';
import { createHighlighter } from 'shiki';
const prettyCodeOptions: Options = {
theme: 'github-dark',
getHighlighter: (options) =>
createHighlighter({
...options
}),
onVisitLine(node) {
// Prevent lines from collapsing in `display: grid` mode, and allow empty
// lines to be copy/pasted
if (node.children.length === 0) {
node.children = [{ type: 'text', value: ' ' }];
}
},
onVisitHighlightedLine(node) {
if (!node.properties.className) {
node.properties.className = [];
}
node.properties.className.push('line--highlighted');
},
onVisitHighlightedChars(node) {
if (!node.properties.className) {
node.properties.className = [];
}
node.properties.className = ['word--highlighted'];
}
};
export const authors = defineCollection({
name: 'author',
directory: 'content',
include: '**/authors/*.mdx',
schema: (z) => ({
ref: z.string(),
name: z.string().default('Anonymous'),
avatar: z.string().url().default('')
})
});
export const posts = defineCollection({
name: 'post',
directory: 'content',
include: '**/blog/*.mdx',
schema: (z) => ({
title: z.string(),
description: z.string(),
published: z.string().datetime(),
category: z.string().default('Miscellaneous'),
author: z.string()
}),
transform: async (data, context) => {
const body = await compileMDX(context, data, {
remarkPlugins: [
remarkGfm, // GitHub Flavored Markdown support
codeImport // Enables code imports in markdown
],
rehypePlugins: [
rehypeSlug, // Automatically add slugs to headings
rehypeAutolinkHeadings, // Auto-link headings for easy navigation
[rehypePrettyCode, prettyCodeOptions]
]
});
const author = context
.documents(authors)
.find((a) => a.ref === data.author);
return {
...data,
author,
slug: `/${data._meta.path}`,
slugAsParams: data._meta.path.split(path.sep).slice(1).join('/'),
body: {
raw: data.content,
code: body
}
};
}
});
export const docs = defineCollection({
name: 'doc',
directory: 'content',
include: '**/docs/*.mdx',
schema: (z) => ({
title: z.string(),
description: z.string()
}),
transform: async (data, context) => {
const body = await compileMDX(context, data, {
remarkPlugins: [
remarkGfm, // GitHub Flavored Markdown support
codeImport // Enables code imports in markdown
],
rehypePlugins: [
rehypeSlug, // Automatically add slugs to headings
rehypeAutolinkHeadings, // Auto-link headings for easy navigation
[rehypePrettyCode, prettyCodeOptions]
]
});
return {
...data,
slug: `/${data._meta.path}`,
slugAsParams: data._meta.path.split(path.sep).slice(1).join('/'),
body: {
raw: data.content,
code: body
}
};
}
});
export default defineConfig({
collections: [authors, posts, docs]
});
Add the stylesheet @/app/mdx.css
[data-rehype-pretty-code-figure] code {
@apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0;
counter-reset: line;
box-decoration-break: clone;
}
[data-rehype-pretty-code-figure] [data-line] {
@apply inline-block min-h-[1rem] w-full px-4 py-0.5;
}
[data-rehype-pretty-code-figure] [data-line-numbers] [data-line] {
@apply px-2;
}
[data-rehype-pretty-code-figure] .line-highlighted span {
@apply relative;
}
[data-rehype-pretty-code-title] {
@apply mt-2 px-4 pt-6 text-sm font-medium text-foreground;
}
[data-rehype-pretty-code-title] + pre {
@apply mt-2;
}
We are set and done! Only content is missing now.
Download the last version of the repository and copy following content
- @/app/(marketing)/*
- @/components/marketing/*
- @/lib/markdown/get-table-of-contents.ts
- @/content/*
As a last step you can optionally link back from your auth pages changing @/components/auth/auth-container.tsx
by wrapping the logo in a link
<Link href={Routes.Root}>
<Logo className="justify-center" />
</Link>
And add the legal links in your sign up page.
We are done! You just added ~9k lines of website code.