Johnson Documentation

01 · Overview

Introduction

Everything you need to know before you open your editor — what’s in the box, what it’s built with, and what it expects from your machine.

Welcome

Thanks for picking up Johnson, a bold, single-person portfolio template for designers and creatives, built with Astro. It pairs oversized display type with soft pastel colour, playful GSAP motion, and a case-study layout that lets your work carry the page — with a home page, about page, contact page, auto-generated project case-study pages, and a 404 page. This guide walks through installing the template, understanding how the project is organized, customizing content and styling, and shipping it to production. It’s written for developers who are comfortable with the command line but new to this specific codebase — if you already know Astro well, feel free to skip ahead using the sidebar.

What’s Included

Source files

The full, uncompiled project source — components, pages, styles, and configuration.

Production build

Not pre-built — the package ships source only. Run npm run build to generate a deployable dist/ folder (see Build & Preview).

Design assets

The General Sans variable font, the logo, arrow and looped-text SVGs, inline SVG social icons, and placeholder sample imagery for the hero, about page, and five demo case studies.

This documentation

A single offline HTML file — open it directly in any browser, no server required.

Features

  • Five page types: home (hero + selected work), about (bio, experience, certifications, skills marquee, process steps), contact, project case-study pages, and a 404 page.
  • A work content collection — each case study is one JSON file in src/content/work/ with schema-validated fields; project pages and their prev/next links are generated automatically.
  • GSAP motion: a magnetic nav with a slide-in overlay menu, a scroll-driven colour shift behind the work list, stacked process cards, a looping word ticker, skill marquees, and hover micro-interactions. Decorative loops respect prefers-reduced-motion.
  • Optimized, responsive images through Astro’s built-in astro:assets, plus an image lightbox on case-study pages.
  • SEO-ready: per-page title and description, Open Graph and Twitter tags, canonical URLs, and a sitemap via @astrojs/sitemap.
  • Accessible: real buttons for the menu, focus management, Esc to close the menu and lightbox, visible focus styles, and labelled form fields.

Technology Stack

Johnson is plain Astro with vanilla JavaScript and plain CSS — no TypeScript, UI framework, or CSS preprocessor. The only runtime dependency besides Astro is GSAP.

Astro 7 JavaScript CSS GSAP @astrojs/sitemap Vite

Requirements

ToolMinimum versionNotes
Node.js22.12.0 or laterRequired by Astro 7 (the engines field in package.json)
Package managernpm 9+, pnpm 8+, or Yarn 1.22+The template ships an npm package-lock.json; pnpm and Yarn also work
Code editor—VS Code with the Astro and ESLint extensions is a comfortable default
Git2.xOnly required if you plan to version-control your customized copy

Browser Support

The built site targets the last two versions of all evergreen browsers:

Chrome Firefox Safari Edge
Internet Explorer

Internet Explorer 11 is not supported. The template ships no polyfills — it relies on modern features such as the native <dialog> element (used by the lightbox).

02 · Setup

Getting Started

From a downloaded ZIP to a running dev server in five steps.

Installation & Extraction

  1. Download the template ZIP from your purchase confirmation email or account downloads page.
  2. Extract it to a folder on your machine — avoid folder names with spaces or special characters.
  3. Open that folder in your terminal and in your code editor.
bash
# Extract on macOS / Linux
unzip johnson-astro-package.zip
cd johnson-astro-package/johnson-astro

The ZIP contains a johnson-astro-package/ folder holding the project itself (johnson-astro/) and a documentation.txt file that links to this guide. Run every command below from inside johnson-astro/.

Node.js Setup

Check which version of Node.js is currently active:

bash
node --version
# v22.12.0 or later

If you manage multiple Node versions, tools like nvm or fnm make switching painless:

bash
nvm install 22
nvm use 22

Install Dependencies

From the project root, install packages with your preferred package manager:

npm
npm install
pnpm
pnpm install
yarn
yarn install

This reads package.json and downloads every dependency into a local node_modules/ folder. It only needs to be re-run when dependencies change.

Run the Development Server

npm
npm run dev
pnpm
pnpm dev

This starts Vite’s local dev server with hot module reloading — edits to components, pages, and styles appear in the browser immediately without a manual refresh. By default it’s available at http://localhost:4321; the terminal output will confirm the exact address.

Build for Production & Preview

bash
# Compile an optimized production build into dist/
npm run build

# Serve that build locally to sanity-check it
npm run preview

build validates the content collection, bundles and minifies assets, optimizes images, generates the sitemap, and writes static output to dist/. preview serves that exact output locally so you can verify it before deploying — it does not rebuild automatically, so re-run build after further changes.

Which command do I actually need?

Use dev while you’re customizing the template. Use build, then preview, once before you deploy — it catches issues (like a missing import on a page you haven’t opened in the browser) that the dev server only reports on demand but the build won’t let through.

03 · Reference

Project Structure

A tour of the folders you’ll actually touch, and the ones you generally shouldn’t.

PathPurpose
public/Files here are copied to the output root untouched — no optimization or hashing. Johnson keeps the favicons, the General Sans font file, and small SVG/PNG icons here.
src/components/The page sections (Hero, WorkList, AboutMe, ProcessSteps, ContactForm…) and shared UI (Navbar, Footer, ArrowLink, SocialLinks…). See Components.
src/layouts/BaseLayout.astro — the single page shell with all <head> tags, the navbar, footer, scroll indicator, and floating mail button.
src/pages/Every file here becomes a route automatically. See Pages for details.
src/content/work/Case-study entries for the work content collection — one JSON file per project. See Content Editing.
src/assets/Images that are optimized, resized, and hashed by the build. See Images & Assets.
src/styles/Four plain CSS files imported by BaseLayout, including the colour variables in global.css. See Styling.
src/data/Site-wide content kept separate from markup: site.js (name, email, tagline, nav links, social links) and about.js (experience, certifications, skills, process steps).
Files not shown above

src/content.config.js defines the work collection and its schema. package-lock.json pins exact dependency versions for npm, and .vscode/ recommends the Astro editor extension. Generated folders (node_modules/, dist/, .astro/) appear after you install and build — never edit them by hand.

04 · Reference

Configuration

The files that control how the project builds, and where to set site-wide values.

package.json

Defines dependencies and the scripts you run from the terminal (dev, build, preview). Add your own script here if you introduce a new tool.

jsonpackage.json
{
  "name": "johnson-astro",
  "type": "module",
  "version": "0.0.1",
  "engines": { "node": ">=22.12.0" },
  "scripts": {
    "dev": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "astro": "astro"
  },
  "dependencies": {
    "@astrojs/sitemap": "^3.7.4",
    "astro": "^7.3.4",
    "gsap": "^3.15.0"
  }
}

astro.config.mjs

Controls integrations, the deployed site URL, and build behavior. Johnson registers only the sitemap integration and builds as a fully static site (Astro’s default). Set site to your real domain before deploying — it’s used for canonical URLs, Open Graph image URLs, and the sitemap.

javascriptastro.config.mjs
// @ts-check
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  // Replace with your deployed URL — used for canonical URLs, OG images and the sitemap.
  site: 'https://example.com',
  integrations: [sitemap()],
});

tsconfig.json

Johnson contains no TypeScript code, but Astro projects keep a tsconfig.json so your editor understands .astro files and the generated content-collection types. It just extends Astro’s preset — no path aliases are defined, so imports use relative paths like ../components/Hero.astro.

jsontsconfig.json
{
  "extends": "astro/tsconfigs/strict",
  "include": [".astro/types.d.ts", "**/*"],
  "exclude": ["dist"]
}

Environment Variables (.env)

Johnson doesn’t read any environment variables out of the box — site details live in src/data/site.js instead. If you add an integration that needs one (a form service key, an analytics ID), create a .env file in the project root for values that differ between environments or shouldn’t be committed to Git. Variables must be prefixed PUBLIC_ to be readable in client-side code.

env.env
# Exposed to the browser — never put secrets here
PUBLIC_SITE_URL=https://example.com

# Server-only — safe for secrets, not readable in browser code
API_SECRET_KEY=your_secret_key
Never commit .env

Add .env to .gitignore (it usually is by default). Anything without the PUBLIC_ prefix should stay off the client entirely.

05 · Guide

Pages

How routes are created, and how the case-study pages are generated.

Creating a New Page

Astro uses file-based routing: any .astro or .md file placed in src/pages/ automatically becomes a route matching its file path. Johnson ships these routes:

text
src/pages/index.astro          →  /          (hero + selected work)
src/pages/about.astro          →  /about
src/pages/contact.astro        →  /contact
src/pages/project/[slug].astro →  /project/:slug/  (one per case study)
src/pages/404.astro            →  shown for any unknown URL

To add a page, create a new file in src/pages/ using the layout and structure of an existing, similar page as a starting point:

astrosrc/pages/services.astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout title="Services" description="What I can help you with.">
  <section class="hero-section">
    <h1 class="display">Services</h1>
  </section>
</BaseLayout>

If the new page should appear in the menu, add it to navLinks in src/data/site.js (see Navigation).

Editing an Existing Page

Johnson’s page files are short — they mostly import section components (for example, about.astro renders AboutHero, AboutMe, and ProcessSteps). Most visible copy lives in those components under src/components/ or in the data files under src/data/, so edit the component or data file to change it.

Dynamic Routes

Files wrapped in square brackets, such as [slug].astro, generate multiple pages from a single template. Johnson uses src/pages/project/[slug].astro to build one case-study page per entry in the work content collection. Its getStaticPaths() sorts projects by order and passes each page its previous and next project (wrapping around at the ends):

astrosrc/pages/project/[slug].astro
---
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  const projects = (await getCollection('work')).sort((a, b) => a.data.order - b.data.order);
  return projects.map((project, index) => ({
    params: { slug: project.id },
    props: {
      project,
      prev: projects[(index - 1 + projects.length) % projects.length],
      next: projects[(index + 1) % projects.length],
    },
  }));
}
---

The slug is the JSON file name — src/content/work/beyond-energy.json becomes /project/beyond-energy/.

Layouts

Layouts (in src/layouts/) wrap page content with shared structure — the <html> shell, <head> metadata, header, and footer. Johnson has a single layout, BaseLayout.astro, and every page renders inside it via a <slot />. It accepts title, description, image (the Open Graph image), and two booleans: footer (set to false on the contact and 404 pages) and chrome (hides the navbar, footer, scroll indicator, and mail button entirely). Simplified:

astrosrc/layouts/BaseLayout.astro
---
const { title, description = site.description, image, footer = true, chrome = true } = Astro.props;
const pageTitle = title ? `${title} | ${site.name}` : `${site.name} — ${site.tagline}`;
---
<html lang="en">
  <head><title>{pageTitle}</title> <!-- + meta, OG, Twitter, fonts --></head>
  <body>
    {chrome && <Navbar />}
    <main><slot /></main> <!-- page content is injected here -->
    {chrome && footer && <Footer />}
    {chrome && <ScrollIndicator />}
    {chrome && <MailButton />}
  </body>
</html>

Page Metadata & the 404 Page

Pass a title and description into BaseLayout on every page for correct browser tabs and search results (see also SEO). The title is automatically suffixed with the site name (e.g. “About | Johnson”); the home page omits title and gets “Johnson — Creative Designer” from site.js. src/pages/404.astro is served automatically whenever no route matches.

06 · Guide

Components

The building blocks pages are assembled from, and how data flows into them.

Reusable Components

Components live in src/components/ and encapsulate one piece of UI so it can be reused across pages without copy-pasting markup. Johnson’s 21 components are all .astro files:

Used onComponents
Every page (via BaseLayout)Navbar, Footer, FooterMeta, ScrollIndicator, MailButton
HomeHero, WorkList, WorkCard
AboutAboutHero, AboutMe, AboutTable, SkillMarquee, ProcessSteps
ContactContactForm
Case studiesProjectHero, ProjectBody, ProjectNav, Lightbox
Shared helpersArrowLink (text link with animated arrow), SocialLinks, SocialIcon

Props

Props are the inputs a component accepts, similar to HTML attributes, and are read from Astro.props in the component’s front matter. Johnson uses plain JavaScript, so props aren’t type-declared — check the destructuring line at the top of a component to see what it expects. For example, ArrowLink:

astrosrc/components/ArrowLink.astro
---
const { href, label } = Astro.props;
---

<a href={href} class="link w-inline-block" data-arrow-link>
  <span class="button">{label}</span>
  <span class="link-arrow-wrapper">
    <img src="/images/Link-arrow.svg" alt="" class="link-arrow" width="7" height="8" />
  </span>
</a>

In plain language: href is where the link goes and label is the visible text. Both are expected — there are no defaults, so leaving one out renders an empty link or label rather than raising an error.

Component Composition

Larger sections are composed from smaller components rather than written as one long block of markup. WorkList renders one WorkCard per project (simplified):

astrosrc/components/WorkList.astro
<div role="list" class="work-blocks-list">
  {projects.map((project) => <WorkCard project={project} />)}
</div>

Passing Data to Components

In Astro, props are passed the same way as HTML attributes. The home page loads the work collection and hands it to WorkList:

astrosrc/pages/index.astro
---
import { getCollection } from 'astro:content';
import BaseLayout from '../layouts/BaseLayout.astro';
import Hero from '../components/Hero.astro';
import WorkList from '../components/WorkList.astro';

const projects = (await getCollection('work')).sort((a, b) => a.data.order - b.data.order);
---

<BaseLayout>
  <Hero />
  <WorkList projects={projects} />
</BaseLayout>

WorkList also accepts optional heading (default “Selected work..”), headingTag, and moreLink ({ href, label }) props if you reuse it elsewhere.

Where the interactivity lives

There are no framework islands in Johnson. Each animated component carries its own <script> block that imports GSAP (and plugins like ScrollTrigger) — Astro bundles and loads these automatically on any page that uses the component. To change or remove an animation, edit that component’s script.

07 · Guide

Styling

Where to change colors, type, and spacing without fighting the build.

Global Styles & CSS Variables

Johnson uses plain CSS split across four files in src/styles/, all imported (in this order) at the top of BaseLayout.astro:

FilePurpose
normalize.cssnormalize.css v3 browser reset.
base-framework.cssBase layout/utility classes (the w-* classes such as w-container and w-form) and a small embedded icon font.
global.cssThe design itself: colour variables, the General Sans @font-face, type classes, and every section’s styles.
overrides.cssSmall additions on top of global.css, kept separate so the design stylesheet stays intact. A good place for your own overrides.

The colour palette is defined once as CSS custom properties at the top of global.css and referenced throughout. Change a value here and it updates across the whole site:

csssrc/styles/global.css
:root {
  --orange-light: #ffe8e7; /* page background */
  --heading: #181b2e;      /* headings and default text */
  --body: #3a3d53;         /* body copy */
  --white: white;
  --orange: #fe5621;       /* accent */
}

Each case study also has its own accent colour, set in its JSON file (see Content Editing).

Component Styles

Astro components support scoped <style> blocks — CSS written there only applies to that component, so class names never collide across files. Most of Johnson’s styling lives in global.css; a few components (Lightbox, the 404 page) use a scoped block, and others (Navbar, Hero, AboutHero, SkillMarquee) use <style is:global> for small adjustments that need to reach global classes.

astrosrc/pages/404.astro
<style>
  .utility-page-content {
    width: min(560px, 100%);
    padding: 0 20px;
  }
</style>

Responsive Design, Typography & Spacing

Typography is set through reusable classes in global.css — .display (the oversized headline), .h1–.h6, and .paragraph-big, .paragraph, .paragraph-small, .paragraph-mini. Responsive overrides live in three @media blocks near the end of the file at max-width: 991px (tablet), 767px (mobile landscape), and 479px (mobile portrait). Prefer adjusting these shared classes over overriding sizes on individual elements, so the site keeps a consistent rhythm as you edit it.

Avoid editing dist/

The dist/ folder is generated output — any change made there is overwritten on the next build. Always edit files under src/.

08 · Guide

Images & Assets

Two homes for static files, and when to use each.

LocationUse forBehavior
src/assets/Images referenced inside components or pages — images/ holds the hero, about, and process-step pictures; work/ holds case-study thumbnails and gallery imagesOptimized, resized, and content-hashed automatically by the build
public/Files that need a fixed, predictable URL — the favicons, fonts/GeneralSans-Variable.ttf, and the small icons in images/ (logo, arrows, looped text, mail icon)Copied as-is, no processing

Replacing Images

Swap the file in place (keeping the same filename) or update the import/path to point at your new file. For example, the portrait used in the hero, footer, and contact page is imported like this:

astrosrc/components/Hero.astro
---
import { Image } from 'astro:assets';
import heroImage from '../assets/images/Hero-Image.webp';
---
<Image src={heroImage} alt="" class="hero-image" width={332} loading="eager" />

Case-study images are referenced by relative path from each project’s JSON file (e.g. "thumbnail": "../../assets/work/thumb-2.webp") — drop your images into src/assets/work/ and update those paths. The default Open Graph image is src/assets/images/About-Me-Image.webp, set in BaseLayout.astro; case-study pages use their own mainImage instead.

Sample imagery is placeholder only

The bundled photos and case-study images are demo content. Replace them with your own work before publishing.

Supported Formats & SVGs

JPEG, PNG, WebP, AVIF, and GIF are supported through the optimized Image component — Johnson’s photos are mostly WebP. Its decorative SVGs (logo, arrows, looped text) live in public/images/ and are referenced as plain <img> sources; the social icons are inline SVG markup in SocialIcon.astro so they can inherit colour via currentColor.

Image Optimization

Images imported from src/assets/ are automatically resized and re-encoded at build time based on the width/height (or densities/widths) you specify — you generally don’t need a separate compression step before adding an image to the project.

Rule of thumb

If an image is part of the design (hero photos, thumbnails, icons), put it in src/assets/. If a URL needs to work regardless of the build (a favicon, a file linked from an email), put it in public/.

09 · Guide

Fonts & Icons

Swapping typefaces and icon sets without touching every component.

Google Fonts

Johnson uses two typefaces: General Sans (self-hosted, the body font) and Outfit (weight 500, from Google Fonts, used for the display and heading classes, buttons, and navigation links). The Outfit <link> tag lives in BaseLayout.astro’s <head>. To swap it, replace the embed code and then update the font-family: Outfit, sans-serif declarations in global.css (there’s no shared font variable, so search the file for “Outfit”):

htmlsrc/layouts/BaseLayout.astro
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@500&display=swap" rel="stylesheet" />

Local Fonts

General Sans is self-hosted as a single variable font file at public/fonts/GeneralSans-Variable.ttf, preloaded in BaseLayout.astro and declared with @font-face at the top of global.css:

csssrc/styles/global.css
@font-face {
  font-family: Generalsans Variable;
  src: url('/fonts/GeneralSans-Variable.ttf') format("truetype");
  font-weight: 200 700;
  font-style: normal;
  font-display: swap;
}

To use a different self-hosted font, put the file in public/fonts/, update this rule and the body font-family, and update the <link rel="preload"> path in BaseLayout.astro. General Sans is distributed under the ITF Free Font License; Outfit under the SIL Open Font License.

Icon Libraries

Johnson doesn’t use an icon library. Its icons are a small set of hand-picked SVGs: the social icons (Dribbble, Facebook, X, Skype) are inline SVG strings in src/components/SocialIcon.astro, and the logo, arrows, and mail icon are image files in public/images/. The menu and close icons are inline SVGs in Navbar.astro.

Replacing & Adding Icons

To add a social icon, paste its SVG markup into the icons object in SocialIcon.astro under a new key (use fill="currentColor" so it picks up the theme colour), then reference that key as the icon value of an entry in socialLinks in src/data/site.js. To replace the logo or arrows, overwrite the matching file in public/images/, keeping the filename.

10 · Beginner Friendly

Content Editing

The fastest, lowest-risk way to make the site say what you want it to say.

New to code?

Everything in this section is plain text or attribute changes — no logic to break. Save the file, and your dev server (from Getting Started) will refresh the browser automatically so you can see the change right away.

Headings, Paragraphs & Buttons

Start with src/data/site.js — it holds your name, tagline, meta description, and email, which are reused in the hero, page titles, footer, and mail button:

javascriptsrc/data/site.js
export const site = {
  name: 'Johnson',
  author: 'Johnson Smith',
  tagline: 'Creative Designer',
  description:
    'Hello, I’m Johnson Smith. A creative designer with a passion for crafting visual experiences and creative websites.',
  email: 'hello@example.com',
};

For text that isn’t in a data file, open the matching component in src/components/, find the text between the tags, and edit it directly. For example, the home hero:

astrosrc/components/Hero.astro
<!-- before -->
<p class="paragraph-big">A creative designer with a passion for crafting visual experiences and creative websites.</p>
<ArrowLink href="/about" label="Read More" />

<!-- after -->
<p class="paragraph-big">A brand and product designer helping small teams launch with confidence.</p>
<ArrowLink href="/about" label="About me" />

The two halves of the big hero headline (“Creative” / “— Designer”) are the two <span class="display"> elements just above that paragraph.

Menu links and social links are kept as lists in src/data/site.js (navLinks and socialLinks), so you edit the label and URL once rather than hunting through every page. The footer’s copyright line and its About/Contact links are in src/components/FooterMeta.astro. See Navigation for the full pattern.

Case Studies & About-Page Lists

Repeating content blocks are driven by data rather than hand-written per card, so adding or removing an item is a matter of editing a file. Each case study is one JSON file in src/content/work/; its fields are validated against the schema in src/content.config.js:

jsonsrc/content/work/beyond-energy.json
{
  "title": "Beyond Energy",
  "order": 2,
  "summary": "A web project that brings together luxury, elegance, and sophistication.",
  "category": "Branding",
  "role": "Development",
  "year": "2023",
  "accent": "#d6f0ff",
  "liveUrl": "https://example.com",
  "thumbnail": "../../assets/work/thumb-2.webp",
  "mainImage": "../../assets/work/main.webp",
  "overview": "…",
  "problem": "…",
  "solution": "…",
  "dividerImages": ["../../assets/work/divider-1.webp", "../../assets/work/divider-2.webp"],
  "problemImages": ["../../assets/work/problem-1.webp", "../../assets/work/problem-2.webp"],
  "solutionImages": ["../../assets/work/solution-1.webp", "…"]
}

To add a project, copy one of the JSON files, rename it (the file name becomes the URL slug), change the fields, and point the image paths at your own files. order controls its position in the list and the prev/next sequence; dividerImages must contain exactly two images. To remove a project, delete its JSON file.

The About page’s experience, certifications, skills marquee rows, and process steps are arrays in src/data/about.js — add, remove, or reorder entries there.

Blog Content & Contact Information

Johnson doesn’t include a blog. Contact details live in one place: the email address in site.email (src/data/site.js) is used by the floating mail button and the email link in the footer and on the contact page, and the social profile URLs are the socialLinks array in the same file. The bundled values (hello@example.com, root-domain social URLs) are placeholders — replace them with your own.

12 · Guide

Forms

What ships out of the box, and what still needs a backend.

Forms need somewhere to go

The contact form in this template captures input in the browser, but submitting it to an email inbox, database, or mailing list requires a backend endpoint or a third-party service — this is not included by default. Johnson’s form is not wired to any service: its action is "#", and a small demo script just shows the success message instead of sending anything.

Contact Forms

The contact form lives in src/components/ContactForm.astro (rendered on /contact) and has three fields: name, email, and message. To make it send real submissions, point the form’s action at your endpoint:

astrosrc/components/ContactForm.astro
<!-- before -->
<form name="contact" method="post" action="#" class="contact-form" data-contact-form>

<!-- after (example: Formspree) -->
<form name="contact" method="post" action="https://formspree.io/f/your-form-id" class="contact-form" data-contact-form>

Then delete the demo <script> at the bottom of the same file. (It already stops intercepting submissions once action is anything other than "#", but it’s demo-only code.)

Newsletter Forms

Johnson doesn’t include a newsletter form, and no email-marketing provider is pre-wired. If you add one, follow the same pattern as the contact form — a plain HTML form posting to your provider’s embed URL.

Form Validation

Validation uses standard HTML attributes only, checked by the browser before submission: name and email are required, email uses type="email", and fields have maxlength limits (256 for name/email, 5000 for message). The message field is optional as shipped — add required to its <textarea> if you want it mandatory. Each field has a visually hidden <label> for screen readers.

Third-Party Form Services

If you’d rather not run your own backend, services like Formspree, Getform, or Basin accept form submissions directly from static HTML and forward them to your email — typically a one-line change to the form’s action attribute.

13 · Guide

API & Data

Only relevant if your template fetches data from an external source — skip this section otherwise.

Johnson fetches no external data

Johnson makes no API calls and needs no keys. All content comes from local files — src/data/site.js, src/data/about.js, and the work content collection — and is baked into static HTML at build time. The rest of this section is only a reference in case you add an external source yourself.

API Configuration

If you add an API, read its URL and keys from environment variables (see Configuration) rather than hard-coding them, so the same code can point at a staging or production API depending on environment:

env.env
PUBLIC_API_URL=https://example.com/api
PUBLIC_API_KEY=your_api_key
Never expose real credentials

The values above are placeholders. Keep real API keys out of version control, and prefer server-only variables (without the PUBLIC_ prefix) for anything sensitive.

Fetch Requests

javascript
const res = await fetch(`${import.meta.env.PUBLIC_API_URL}/products`, {
  headers: { Authorization: `Bearer ${import.meta.env.PUBLIC_API_KEY}` },
});
const data = await res.json();

Data Structures

Johnson’s only structured data is local. The work collection’s shape is enforced by a Zod schema in src/content.config.js; a JSON file that doesn’t match it fails the build with an error naming the bad field:

javascriptsrc/content.config.js
const work = defineCollection({
  loader: glob({ pattern: '**/*.json', base: './src/content/work' }),
  schema: ({ image }) =>
    z.object({
      title: z.string(),
      order: z.number(),
      summary: z.string(),
      category: z.string(),
      role: z.string(),
      year: z.string(),
      accent: z.string().default('#d6f0ff'),
      liveUrl: z.string().default('https://example.com'),
      thumbnail: image(),
      mainImage: image(),
      overview: z.string(),
      problem: z.string(),
      solution: z.string(),
      dividerImages: z.array(image()).length(2),
      problemImages: z.array(image()),
      solutionImages: z.array(image()),
    }),
});

The objects in src/data/site.js and src/data/about.js aren’t validated — keep each entry’s keys the same as the existing ones (for example, every experience item needs period, role, company, and href).

Error Handling

javascript
try {
  const res = await fetch(apiUrl);
  if (!res.ok) throw new Error(`Request failed: ${res.status}`);
  const data = await res.json();
} catch (err) {
  // show a fallback UI state instead of a blank page
  console.error(err);
}
14 · Guide

SEO

Per-page metadata, social previews, and the machine-readable files search engines expect.

Page Title & Meta Description

Pass a unique title and description into BaseLayout on every page — these drive both the browser tab and the snippet shown in search results. The layout appends the site name for you, so pass just the page name:

astrosrc/pages/about.astro
<BaseLayout
  title="About"
  description="About Johnson Smith — experience, certifications, skills and design process."
>
<!-- renders <title>About | Johnson</title> -->

If description is omitted, site.description from src/data/site.js is used. Case-study pages use each project’s title and summary automatically.

Canonical URL, Open Graph & Twitter/X Cards

Already set up inside BaseLayout.astro’s <head>, using the site value from astro.config.mjs to build absolute URLs. The Open Graph image is generated at build time as a 1200px-wide JPG from the page’s image prop, or from About-Me-Image.webp by default:

astrosrc/layouts/BaseLayout.astro
<link rel="canonical" href={canonical} />
<meta property="og:type" content="website" />
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={description} />
<meta property="og:image" content={ogImage} />
<meta property="og:url" content={canonical} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={ogImage} />

These URLs are only correct once site in astro.config.mjs is changed from https://example.com to your real domain.

Sitemap & Robots.txt

Johnson includes Astro’s official sitemap integration (@astrojs/sitemap), which generates sitemap-index.xml automatically at build time from the site value, and BaseLayout links to it. No robots.txt is bundled — if you want one, create public/robots.txt to control crawler access and point it at the sitemap:

textpublic/robots.txt
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap-index.xml

Structured Data & Semantic HTML

Use landmark elements (<header>, <nav>, <main>, <article>) and a single <h1> per page so both search engines and screen readers can parse the page’s structure — Johnson’s pages each have a single <h1> inside a <main> landmark. No JSON-LD structured data ships with the template; if you want rich results, add it in BaseLayout.astro’s <head> (a Person schema suits a personal portfolio).

15 · Guide

Deployment

Platform-by-platform steps. Confirm your rendering mode first — it changes the build output you deploy.

Static vs. SSR — check this first

A static Astro build (the default) produces plain HTML/CSS/JS in dist/ that any static host can serve. An Astro site using server-side rendering (SSR via an adapter) needs a Node-capable host or the platform’s matching adapter. Johnson is fully static — no adapter is installed — so use the static instructions below.

Vercel

  1. Push the project to a GitHub, GitLab, or Bitbucket repository.
  2. In Vercel, choose Add New → Project and import that repository.
  3. Build command: npm run build · Output directory: dist (Astro static) — Vercel’s Astro preset detects this automatically in most cases.
  4. If you’ve added a .env file, add its variables under Project Settings → Environment Variables (Johnson needs none as shipped).
  5. Click Deploy.

Netlify

tomlnetlify.toml
[build]
  command = "npm run build"
  publish = "dist"

Connect the repository in the Netlify dashboard, confirm these build settings, add any environment variables you’ve introduced under Site configuration → Environment variables, and deploy. Johnson doesn’t ship a netlify.toml — create one with the contents above if you want the settings in the repo.

Cloudflare Pages

Connect the repository, set the build command to npm run build and the output directory to dist. Johnson is static, so no adapter is needed. Make sure the build environment uses Node 22.12 or later (set a NODE_VERSION variable if the default is older).

GitHub Pages

Set site and base in astro.config.mjs to match your repository URL, then build and publish the dist/ folder to the gh-pages branch (via the official astro build + a deploy action, or manually). GitHub Pages only serves static output — it cannot run an SSR adapter.

Traditional Hosting

Run npm run build locally, then upload the contents of dist/ to your host’s public web root via FTP/SFTP or your host’s file manager. Because Johnson is fully static, any plain shared host works — no Node process is needed on the server.

16 · Guide

Build & Production

What happens when you run build, and a checklist before you ship.

bash
npm run build     # bundle, optimize images, and write dist/
npm run preview   # serve dist/ locally, exactly as it will run in production

Build Output

The build writes static HTML (one folder per route, e.g. dist/project/beyond-energy/index.html), hashed CSS/JS bundles, optimized images under dist/_astro/, the public/ files, and sitemap-index.xml to dist/. This folder is disposable and regenerated by every build — Johnson’s .gitignore already keeps it out of Git.

Environment Variables in Production

Johnson needs no environment variables as shipped. If you add some, remember that variables from your local .env are not deployed automatically — each hosting platform requires you to set them again in its dashboard (see Deployment). A missing production variable is one of the most common causes of a build that works locally but fails, or behaves unexpectedly, once deployed.

Production Checklist

  • Run npm run build locally and confirm it completes with no errors.
  • Run npm run preview and click through the home, about, contact, and case-study pages.
  • Change the site URL in astro.config.mjs from https://example.com to your real domain.
  • Replace the placeholder name, email, and social links in src/data/site.js, and the résumé content in src/data/about.js.
  • Point the contact form’s action at a real endpoint and remove its demo script (see Forms).
  • Replace the sample images and case studies with your own work.
  • Update the copyright line in FooterMeta.astro, and verify the favicon, logo, Open Graph image, and meta descriptions are your own, not the template defaults.
  • Set any environment variables you’ve added in your hosting platform’s dashboard.
17 · Reference

Troubleshooting

The most common problems, in the order you’re likely to hit them.

npm install fails

Cause
Node.js older than 22.12 (Astro 7’s minimum), a corrupted local cache, or a stale lockfile from a different package manager.
Solution
Confirm node --version reports v22.12.0 or later (see Requirements), then run rm -rf node_modules package-lock.json && npm install.

Port already in use

Cause
Another process (often a previous dev server that didn’t shut down cleanly) is already listening on the default port.
Solution
Stop the other process, or start this one on a different port: npm run dev -- --port 4322.

Module not found

Cause
A missing dependency (e.g. gsap), or a typo in a relative import path — Johnson uses relative paths like ../components/Hero.astro and no path aliases.
Solution
Re-run npm install; check the number of ../ segments and that the path casing matches the file exactly (e.g. Hero-Image.webp, not hero-image.webp — case matters on Linux hosts).

Images not loading

Cause
An incorrect relative path, or an image placed in src/assets/ but referenced with a public/-style absolute path (or vice versa).
Solution
Review Images & Assets — imported assets need an import statement; case-study JSON image paths are relative to the JSON file (../../assets/work/…); public/ files need a root-relative URL like /images/Logo.svg.

Styles not loading

Cause
A page isn’t rendered inside BaseLayout (which imports all four stylesheets), or an overrides.css rule is being beaten by a more specific selector in global.css.
Solution
Wrap the page in <BaseLayout>; confirm the four import '../styles/….css' lines are still at the top of BaseLayout.astro; use your browser’s inspector to see which rule wins.

Content collection errors

Cause
A case-study JSON file doesn’t match the work schema — a missing field, a number written as a string (e.g. "order": "2"), an image path that doesn’t exist, or a dividerImages array without exactly two items.
Solution
Read the error, which names the file and the field; compare the file against the schema in src/content.config.js (see Data Structures) and an existing project JSON, and fix the mismatch.

Build fails (but dev server works)

Cause
The dev server only processes pages as you open them, while build renders every route and optimizes every image — an issue on a page you didn’t visit in dev can fail the build.
Solution
Read the full terminal output for the first error (later errors are often side effects of it); fix issues top to bottom and re-run.

Environment variables not working

Cause
Only applies if you’ve added your own variables (Johnson uses none): missing the PUBLIC_ prefix for a client-side value, or the dev server wasn’t restarted after editing .env.
Solution
Rename the variable with the PUBLIC_ prefix if it’s used in browser code, then stop and restart npm run dev — env changes aren’t hot-reloaded.

Deployment problems

Cause
Wrong output directory configured on the host, a build image running Node older than 22.12, or site still set to https://example.com (canonical and Open Graph URLs point at the wrong domain).
Solution
Re-check the build command (npm run build) and output folder (dist) against Deployment for your platform, set the host’s Node version to 22.12+, and update site in astro.config.mjs.
18 · Reference

FAQ

Quick answers to the questions we hear most.

How do I install the template?

Extract the ZIP, run your package manager’s install command, then start the dev server. The full walkthrough is in Getting Started.

Which Node.js version should I use?

Node 22.12.0 or later, as required by Astro 7 — see Requirements. Any current LTS release at or above 22.12 is a safe default.

Can I use JavaScript instead of TypeScript?

Johnson is already plain JavaScript — there are no .ts or .tsx files. If you prefer TypeScript, you can add it gradually: Astro supports .ts files and typed props out of the box, and the included tsconfig.json already extends Astro’s strict preset.

How do I change colors?

Update the CSS custom properties at the top of src/styles/global.css (--orange-light, --heading, --body, --orange), and each case study’s accent in its JSON file — see Styling.

How do I change fonts?

Swap the Outfit Google Fonts link and/or the General Sans @font-face rule, then update the matching font-family declarations in global.css — see Fonts & Icons.

How do I add or remove a project?

Copy or delete a JSON file in src/content/work/; the home-page list, case-study page, and prev/next links update automatically — see Content Editing.

Can I turn off the animations?

Visitors who enable “reduce motion” in their OS already skip the decorative loops. To remove an animation entirely, delete or edit the <script> block in the component that owns it (e.g. SkillMarquee.astro for the marquees) — see Components.

How do I replace images?

Swap the file or update the import path — see Images & Assets.

How do I add a new page?

Add a file to src/pages/; the route is created automatically — see Pages.

How do I deploy the template?

Platform-specific steps for Vercel, Netlify, Cloudflare Pages, GitHub Pages, and traditional hosts are in Deployment.

Can I use the template commercially?

Yes. Johnson is sold under the RNN studio commercial license (the LICENSE file in the project root). You may build unlimited websites with it, for yourself or for clients, including commercial sites, and charge for that work. You may not resell, redistribute, or publish the template itself (or a modified version) as a template, theme, or starter — including in a public GitHub repository. Bundled fonts, GSAP, and sample imagery follow their own licenses; see the Credits section of README.md.

Where can I get support?

See the Support section for contact details and what to include in a request.

19 · Reference

Changelog

Every notable change, newest first. Add a new entry above the last one for each release.

Version 0.0.1
  • AddedInitial release: home, about, contact, project case-study, and 404 pages.
  • Addedwork content collection with five sample case studies and auto-generated prev/next navigation.
  • AddedGSAP animations, including the magnetic overlay menu, work-list colour shift, stacked process cards, and skill marquees.
  • AddedSEO meta, Open Graph/Twitter tags, canonical URLs, and sitemap.
Keeping this current

Copy the markup for one entry above, update the version number, date, and bullet list, and place it at the top of this section for each new release.

20 · Reference

Support

Stuck on something this guide didn’t cover? Here’s how to reach us.

Documentation

Most questions are answered somewhere in this guide — try the search (Ctrl K) before reaching out.

Back to top

Contact support

Email hello@rnnstudio.com or reach us through our contact form.

Contact support

Support hours

We’re based in India and available Monday–Friday, 10am–7pm IST. Expect a response within 24 hours.

What to Include in a Support Request

The fastest way to get a useful answer is to include the details below in your first message:

text
When contacting support, please include:

- Template name and version
- Browser (and version)
- Operating system
- Node.js version (node --version)
- The exact error message
- Steps to reproduce the problem