Buy Pro
← All posts

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

Update

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 light landing page bento

Blog Pages

light blog page light blog details page

Docs

light docs page

Pricing Page

light pricing page

Story Page

light story page light terms of use page

...also privacy policy page and cookie policy page.

Careers Page

light careers page

Contact Page

light 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.

Commits