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
![light landing page hero](/_next/image?url=%2Fblog%2Fmarketing-pages%2Flight-landing-page-hero.webp&w=3840&q=100)
![light landing page bento](/_next/image?url=%2Fblog%2Fmarketing-pages%2Flight-landing-page-bento.webp&w=3840&q=100)
Blog Pages
![light blog page](/_next/image?url=%2Fblog%2Fmarketing-pages%2Flight-blog-page.webp&w=3840&q=100)
![light blog details page](/_next/image?url=%2Fblog%2Fmarketing-pages%2Flight-blog-details-page.webp&w=3840&q=100)
Docs
![light docs page](/_next/image?url=%2Fblog%2Fmarketing-pages%2Flight-docs-page.webp&w=3840&q=100)
Pricing Page
![light pricing page](/_next/image?url=%2Fblog%2Fmarketing-pages%2Flight-pricing-page.webp&w=3840&q=100)
Story Page
![light story page](/_next/image?url=%2Fblog%2Fmarketing-pages%2Flight-story-page.webp&w=3840&q=100)
Legal Pages
![light terms of use page](/_next/image?url=%2Fblog%2Fmarketing-pages%2Flight-terms-of-use-page.webp&w=3840&q=100)
...also privacy policy page and cookie policy page.
Careers Page
![light careers page](/_next/image?url=%2Fblog%2Fmarketing-pages%2Flight-careers-page.webp&w=3840&q=100)
Contact Page
![light contact page](/_next/image?url=%2Fblog%2Fmarketing-pages%2Flight-contact-page.webp&w=3840&q=100)
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.