Skip to content

07_DOCS-SITE

A step-by-step guide for deploying a lightweight, client-side documentation site on any domain. Zero build tools, zero server processes, zero backend dependencies — just static files served by Apache/Nginx.


What You Get

A documentation site at a subdomain (e.g., docs.yourdomain.com) that:

  • Renders .md files client-side using marked.js (CDN)
  • Has a two-column layout: sidebar navigation + content area
  • Supports hash-based routing (#section/page-name)
  • Includes real-time sidebar search
  • Has syntax-highlighted code blocks via highlight.js
  • Is fully responsive (mobile sidebar overlay)
  • Deploys as static files alongside your existing app
  • Requires no build step — edit markdown, push, done

File Structure

docs-site/
  index.html              # Page shell — nav, sidebar, content area
  style.css               # Prose typography + sidebar styles
  app.js                  # Hash router, markdown renderer, sidebar builder
  docs.json               # Navigation manifest (drives sidebar)
  .htaccess               # Apache: allow .md files to be served (see Gotchas)
  content/
    section-a/
      first-doc.md        # Your markdown content
      second-doc.md
    section-b/
      another-doc.md

~7-8 files to start, ~700 lines total. No existing application files modified.


Step 1: Create the Page Shell (index.html)

The single HTML file that loads everything. Key components:

html
<!-- Tailwind CDN (play mode) with your brand colors -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
    tailwind.config = {
        theme: {
            extend: {
                colors: {
                    primary: { /* your brand color scale */ },
                    accent: { /* your accent color scale */ },
                }
            }
        }
    }
</script>

<!-- Alpine.js for sidebar toggle + reactive state -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

<!-- Marked.js for markdown parsing -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>

<!-- Highlight.js for syntax highlighting -->
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github.min.css">

<link rel="stylesheet" href="style.css">

Body Layout

┌─────────────────────────────────────────────┐
│  Top Nav: Brand + "Docs" badge + ext links  │
├──────────┬──────────────────────────────────┤
│ Sidebar  │                                  │
│ (w-72)   │  Content Area (rendered markdown)│
│          │                                  │
│ - Search │  - Breadcrumb                    │
│ - Tabs   │  - Article content               │
│ - Items  │  - Welcome / Loading / 404 state │
│          │                                  │
└──────────┴──────────────────────────────────┘

Key features:

  • Top nav: Sticky, branded, with links back to main site / app
  • Sidebar: Fixed on desktop (lg:sticky), slide-in overlay on mobile with backdrop
  • Section tabs: Toggle between content audiences (e.g., "User Guide" / "Developer")
  • Search input: Filters sidebar items client-side by title
  • Collapsible categories: Group related docs under headings
  • Content area: Three states — welcome (no doc selected), loading spinner, rendered markdown
  • Mobile: Hamburger button toggles sidebar, click overlay to close

The entire shell uses Alpine.js x-data for state management: sidebarOpen, activeSection, currentSlug, searchQuery, renderedHtml, loading, notFound.

Script Tag

html
<script src="app.js"></script>  <!-- At end of body, after Alpine -->

Step 2: Create Typography Styles (style.css)

Since Tailwind CDN play mode doesn't include @tailwindcss/typography, create prose styles manually. Target a wrapper class (e.g., .prose-docs) applied to your content area.

Elements to Style

ElementKey Styles
h1Large, bold, bottom border, margin-top: 0
h2Medium, semibold, generous margin-top
h3, h4Smaller headings with appropriate spacing
p0.75rem vertical margins
aBrand-colored with underline
strong, emAppropriate weight/style
ul, olProper list-style, left padding
blockquoteLeft border (brand color), tinted background
code (inline)Gray background, red text, rounded
preDark background, light text, rounded, overflow-x scroll
pre codeReset inline code styles (transparent bg, inherit color)
tableFull width, collapsed borders, header background
th, tdPadding, bottom borders, hover row highlight
hrSimple top border, generous vertical margin
imgmax-width 100%, rounded

Additional Styles

css
/* Alpine.js cloak */
[x-cloak] { display: none !important; }

/* Sidebar scrollbar (webkit) */
aside::-webkit-scrollbar { width: 4px; }

/* highlight.js override for dark code blocks */
.prose-docs pre .hljs { background: transparent; color: inherit; }

Step 3: Create the Navigation Manifest (docs.json)

A JSON file that defines the sidebar structure. The app fetches this on load.

json
{
  "title": "Your App Docs",
  "sections": [
    {
      "id": "user",
      "label": "User Guide",
      "categories": [
        {
          "label": "Getting Started",
          "items": [
            {
              "slug": "user/quickstart",
              "title": "Quick Start",
              "file": "user/quickstart.md"
            },
            {
              "slug": "user/account",
              "title": "Account Setup",
              "file": "user/account.md"
            }
          ]
        }
      ]
    },
    {
      "id": "dev",
      "label": "Developer",
      "categories": [
        {
          "label": "Architecture",
          "items": [
            {
              "slug": "dev/architecture",
              "title": "System Architecture",
              "file": "dev/architecture.md"
            }
          ]
        }
      ]
    }
  ]
}

To add a new doc: Create the .md file in content/, add one entry to docs.json. That's it.


Step 4: Create the Router + Renderer (app.js)

An Alpine.js component function that handles all client-side logic.

Core Functions

FunctionPurpose
init()Fetch docs.json, configure marked, listen for hashchange
onHashChange()Parse hash, find item in manifest, load doc
findItem(slug)Search manifest sections → categories → items for matching slug
loadDoc(item)Fetch content/{file}, marked.parse(), inject HTML, run hljs.highlightElement() on code blocks
showNotFound()Display "Doc not found" state
navigateTo(item)Set window.location.hash
matchesSearch(item)Check if item title matches search query
filteredHasItems(category)Check if category has any visible items after search filter
activeSectionLabel()Return label of active section for breadcrumb

Router Flow

URL: docs.example.com/#user/quickstart
                         └─── slug


              findItem("user/quickstart")


              fetch("content/user/quickstart.md")


              marked.parse(md) → renderedHtml


              hljs.highlightElement() on <pre><code> blocks

Key Implementation Details

  • Use $nextTick() to run highlight.js after Alpine renders the HTML
  • Update document.title on each navigation
  • Set activeSection based on the slug prefix (e.g., "user" from "user/quickstart")
  • On bare URL (no hash), show a welcome/landing state instead of loading a doc

Step 5: Write Placeholder Content

Create 2-3 markdown docs that exercise all rendering features:

  • Headings (h1-h4)
  • Tables (with header row)
  • Ordered and unordered lists
  • Blockquotes (tips, notes)
  • Inline code and fenced code blocks (with language hints)
  • Bold, italic, links
  • Horizontal rules

This validates your typography styles before adding real content.


Step 6: Deploy to Subdomain

6a. Create the Subdomain (cPanel)

  1. Log into cPanel for your hosting account
  2. Go to DomainsCreate A New Domain
  3. Enter your subdomain: docs.yourdomain.com
  4. Set Document Root to point at your docs-site directory
    • If your docs-site deploys inside your app directory (e.g., via FTP sync), point the document root there: public_html/your-app-dir/docs-site
    • If standalone, use the default cPanel path

Note: cPanel automatically creates the DNS A record and Apache vhost when you create a subdomain. No manual DNS or vhost config needed.

6b. Add .htaccess for .md Files

This is critical. Apache blocks .md files by default (403 Forbidden). Create a .htaccess file in your docs-site root:

apache
# Allow .md files to be served
AddType text/plain .md

<FilesMatch "\.md$">
    Require all granted
</FilesMatch>

Without this, the markdown fetch requests will return 403 and all docs will show "not found."

6c. Push / Deploy Files

Get your files to the server:

  • FTP pipeline (GitHub Actions, etc.): Ensure your docs-site directory is not in the exclude list. Push to trigger deploy.
  • Manual FTP: Upload the entire docs-site/ directory to the document root path.
  • Git-based deploy: Pull on server, files are already in place.

6d. SSL Certificate

If using cPanel/WHM with AutoSSL:

  1. WHM → SSL/TLSAutoSSL
  2. Find your account → Run AutoSSL
  3. AutoSSL automatically picks up the new subdomain

For other environments, use Certbot or your hosting provider's SSL tooling.

6e. Verify

Test in this order:

  1. Raw .md file access: Visit https://docs.yourdomain.com/content/section/doc.md — should show raw markdown text (not 403)
  2. Main page: Visit https://docs.yourdomain.com — should show the docs shell with sidebar
  3. Navigation: Click a sidebar item — hash changes, content loads and renders
  4. Deep link: Paste a full URL like https://docs.yourdomain.com/#section/doc-name — correct doc loads directly

Gotchas & Lessons Learned

Apache blocks .md files (403 Forbidden)

Apache does not serve .md files by default. You must add a .htaccess with AddType text/plain .md and a Require all granted rule for .md files. This is the most common deployment issue.

CSP (Content Security Policy) allowlist

If your main application has a CSP header, make sure it allows the CDN domains used by the docs site:

  • cdn.tailwindcss.com (Tailwind)
  • cdn.jsdelivr.net (Alpine.js, marked.js, highlight.js)

If the docs site is on a separate subdomain, it has its own headers — but if you add CSP via .htaccess or server config, include these.

file:// protocol works for local dev

The docs site works from file:// in a browser during development. Just open docs-site/index.html directly. The only caveat: fetch() for local .md files works in most browsers but may require a local server in some configurations. If it fails, use python -m http.server 8080 in the docs-site/ directory.

Hash routing vs. path routing

This system uses hash routing (#section/page) instead of path routing (/section/page). Benefits:

  • Works with file:// for local dev
  • No server-side routing config needed
  • No FallbackResource or rewrite rules required
  • Deep links work without server cooperation

Tradeoff: URLs have a # in them. For most documentation sites this is perfectly acceptable.

Tailwind CDN play mode

The Tailwind CDN script is "play mode" — it processes classes at runtime. This shows a console warning in production and is slightly slower than a compiled build. For a docs site with minimal traffic, this is a fine tradeoff to avoid a build step.

Adding new docs

The workflow for adding a new documentation page:

  1. Create the .md file in content/{section}/
  2. Add one entry to docs.json under the appropriate section and category
  3. Push / deploy
  4. Done — no build, no restart, no config changes

Optional Enhancements

These are not required but can be added incrementally:

EnhancementDescription
Table of contentsParse rendered <h2>/<h3> tags and build a floating TOC in the right margin
Edit on GitHub linkAdd a link to each doc's source file on GitHub for easy contributions
Version selectorDropdown to switch between doc versions (separate content directories per version)
Print stylesCSS @media print rules to hide sidebar and format content for printing
Last updated dateShow file modification date (requires a build step or API to get git timestamps)
Previous/Next navBottom navigation links based on manifest order
Breadcrumb linksMake breadcrumb segments clickable
Dark modeToggle between light/dark themes with Alpine.js state + CSS variables
Full-text searchIndex all .md content into a JSON file at build time, search client-side
Link to main app navAdd a "Docs" link in your main application's navigation bar

Quick Reference

TaskHow
Add a docCreate .md file + add entry to docs.json
Add a sectionAdd new object to sections[] in docs.json
Add a categoryAdd new object to categories[] in a section
Change brandingEdit tailwind.config colors + nav text in index.html
Change typographyEdit style.css prose rules
DeployPush to trigger FTP sync (or manual upload)
Local previewOpen docs-site/index.html in browser
lock

Enter PIN to continue