Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 30 additions & 20 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

Astro 5 → channel47.dev via Vercel. Static output + one serverless endpoint (`api/subscribe.ts`).

**State:** Fresh slate. Pages were nuked May 2026 to start over. Design system is intact.
**State:** Single-plugin site. Five pages: simple home (4 doors), one plugin page (Field Kit), community (Skool join), subscribe (email capture), contact (solicit work), plus 404. Link prefetching enabled (`prefetch.prefetchAll`).

**Story:** channel47 builds small marketing skills live in the "skills labs" (inside The Vibe Marketers on Skool), then ships them as one Claude Code plugin — **Field Kit**. Skills chain: Customer Research → Persona → Angle (all live) → Page Writer → Section Builder → Convert Check (next lab; output landing pages for Shopify or Next.js).

## Commands

Expand All @@ -18,29 +20,33 @@ npm run preview

## Pages

- `/` — placeholder index
- `/api/subscribe` — POST, proxies to Kit API (preserved)

That's it. Add new pages under `src/pages/`.
- `/` — home: one-line pitch + four "doors" (plugin, labs/Skool, newsletter, contact)
- `/plugin` — Field Kit: hero + install, meta strip, 6 skill rows (3 live / 3 next lab), pipeline, install block, join CTA
- `/community` — Skool join page: hero, stats, what-you-get, member quotes, join CTA. All join CTAs use `SKOOL_URL`
- `/subscribe` — email capture: hero form + "three things in every issue"
- `/contact` — solicit work: services list + project-brief form (email, brief, budget → `/api/subscribe`)
- `/404` — "signal lost" page
- `/api/subscribe` — POST, proxies to Kit API

## Design System (the part that survived)
## Structure

```
src/
├── consts.ts # SKOOL_URL (affiliate — see Gotchas), PLUGIN_INSTALL, SOCIALS. Single source for links
├── layouts/
│ └── BaseLayout.astro # Root layout — meta, fonts, schema, scroll reveal
│ └── BaseLayout.astro # Root layout — meta, fonts, JSON-LD schema, skip link, html.js flag, init scripts
├── components/
│ ├── Nav.astro # Logo-only fixed nav (links stripped, add back as routes ship)
│ ├── Footer.astro # ctrlswing attribution (links stripped)
│ ├── EmailSignup.astro # 3 variants — inline / default / prominent. Posts to /api/subscribe
│ ├── Nav.astro # Fixed glass nav (blur + hairline). Height = --nav-h; body padding-top offsets it
│ ├── Footer.astro # Internal links + socials (from consts), ctrlswing attribution
│ ├── LogoAnimated.astro # Animated channel47 logo
│ └── Analytics/Analytics.astro
├── styles/
│ └── main.css # Tailwind v4 @theme tokens, @layer components, keyframes
│ └── main.css # Tailwind v4 @theme tokens, @layer components, reveal styles, keyframes
└── scripts/
├── scroll-reveal.ts # IntersectionObserver scroll reveal
├── nav-scroll.ts # Nav hide-on-scroll
└── copy-to-clipboard.ts # Copy button utility
├── nav-scroll.ts # Nav hide-on-scroll (currently inert — no .nav--hidden styles defined)
├── subscribe-form.ts # Enhances [data-kit-form] forms; sends extra named controls as `fields`; status via [data-form-status] or live region
└── copy-to-clipboard.ts # Copy button utility ([data-install] → clipboard)
```

`DESIGN.md` at repo root is the canonical design system spec.
Expand All @@ -57,15 +63,18 @@ src/

Tailwind CSS v4 via `@tailwindcss/vite`. Single entry: `src/styles/main.css`.

- `@theme` block defines tokens: warm gray scale, amber accent, font families, radius, animations
- `@layer components` for shared patterns: `.label`, `.wrap`, `.prose`, `.hero`, `.stats`, `.cta`, `.accent-bar`
- Body text is JetBrains Mono. Single accent: amber `#F59E0B`
- shadcn/ui semantic tokens (`background`, `foreground`, `ring`, etc.) defined in `@theme` for React component compatibility
- `@theme` block defines tokens: `bg`/`surface`/`text`/`line` scale, amber accent, font families, radius
- `@layer components` for shared patterns: `.wrap`, `.eyebrow`, `.amber`, `.btn-primary`, `.btn-ghost`, `.head-hero`, `.head-section`, `.head-mid`, `.body-lead`, `.body-mid`, `.section-pad`, `.skip-link`, `.visually-hidden`
- Body text is Geist sans; Geist Mono for code/indices. Single accent: amber `#F5B544`
- `--nav-h` (`:root`) sizes the fixed nav; `body` gets `padding-top: var(--nav-h)` and `[id]` gets matching `scroll-margin-top`
- shadcn/ui semantic tokens (`background`, `foreground`, `ring`) defined in `@theme` for React component compatibility

## Scroll Reveals (two layers)
## Scroll Reveals

1. **Section-level**: `data-section="name"` + IO (threshold 0.15). Adds `.is-visible`.
2. **Element-level**: `BaseLayout` observes `[data-animate]` (threshold 0.1). One-time. Stagger with `data-stagger="1..6"`.
- `data-reveal` — single element, fades up when 12% visible. One-time.
- `data-reveal-stagger` (parent) + `data-reveal-child` (children) — children reveal together with per-child delay via inline `style="--stagger: n"` (capped at 5 in CSS).
- Hidden states are gated behind `html.js` (set inline in BaseLayout `<head>`) and `prefers-reduced-motion: no-preference` — content stays visible without JS or with reduced motion.
- Legacy `data-section` / `data-animate` observers still run but no page uses them.

## Schema (structured data)

Expand All @@ -84,4 +93,5 @@ Tailwind CSS v4 via `@tailwindcss/vite`. Single entry: `src/styles/main.css`.
- **`:global()` required** for cross-component ancestor selectors in scoped styles
- **Scroll reveal hides content by default** — `[data-reveal-child]` and `[data-reveal]` start at `opacity: 0`. Content only appears after IntersectionObserver fires `.is-visible`. If a section looks blank, it's the reveal system, not missing data.
- **Footer attribution** — `ctrlswing`, not `jackson`. Link: `https://x.com/ctrlswing`.
- **Skool affiliate link** — `SKOOL_URL` in `src/consts.ts` ships with a `YOUR_AFFILIATE_REF` placeholder. It MUST be replaced with the real affiliate ref before launch or community signups won't be attributed. Every join CTA reads from this one constant.
- **DataForSEO MCP available** — Use `mcp__dataforseo__*` tools for keyword research.
1 change: 1 addition & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import react from '@astrojs/react';
export default defineConfig({
site: 'https://channel47.dev',
output: 'static',
prefetch: { prefetchAll: true },
adapter: vercel({
webAnalytics: {
enabled: true
Expand Down
54 changes: 0 additions & 54 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 22 additions & 6 deletions src/components/Footer.astro
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
---
import { SOCIALS } from '../consts';

const year = new Date().getFullYear();

const siteLinks = [
{ label: 'Plugin', href: '/plugin' },
{ label: 'Community', href: '/community' },
{ label: 'Subscribe', href: '/subscribe' },
{ label: 'Contact', href: '/contact' },
];
---

<footer class="footer wrap">
<span class="footer__copy">© {year} channel47</span>
<nav class="footer__links" aria-label="Footer">
{siteLinks.map((l) => (
<a href={l.href} class="footer__link">{l.label}</a>
))}
</nav>
<div class="footer__links">
<a href="https://x.com/ctrlswing" target="_blank" rel="noopener noreferrer" class="footer__link">X</a>
<a href="https://linkedin.com/in/ctrlswing" target="_blank" rel="noopener noreferrer" class="footer__link">LinkedIn</a>
<a href="https://github.com/ctrlswing" target="_blank" rel="noopener noreferrer" class="footer__link">GitHub</a>
<a href={SOCIALS.x} target="_blank" rel="noopener noreferrer" class="footer__link">X</a>
<a href={SOCIALS.linkedin} target="_blank" rel="noopener noreferrer" class="footer__link">LinkedIn</a>
<a href={SOCIALS.github} target="_blank" rel="noopener noreferrer" class="footer__link">GitHub</a>
</div>
</footer>

Expand All @@ -17,24 +31,26 @@ const year = new Date().getFullYear();
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
flex-wrap: wrap;
font-size: 12px;
color: var(--color-text-mute);
letter-spacing: 0.18em;
text-transform: uppercase;
border-top: 1px solid var(--color-line);
}
.footer__links { display: flex; gap: 28px; }
.footer__links { display: flex; gap: 28px; flex-wrap: wrap; }
.footer__link {
color: var(--color-text-mute);
transition: color var(--dur-instant);
}
.footer__link:hover { color: var(--color-text); }

@media (max-width: 480px) {
@media (max-width: 640px) {
.footer {
flex-direction: column;
gap: 24px;
align-items: flex-start;
}
.footer__links { gap: 20px; }
}
</style>
63 changes: 39 additions & 24 deletions src/components/Nav.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,58 @@
import LogoAnimated from './LogoAnimated.astro';

interface Props {
active?: 'home' | 'plugins' | 'community' | 'studio' | 'subscribe';
active?: 'home' | 'plugin' | 'community' | 'contact' | 'subscribe';
}

const { active = 'home' } = Astro.props;

const links = [
{ key: 'plugins', label: 'Plugins', href: '/plugins' },
{ key: 'plugin', label: 'Plugin', href: '/plugin' },
{ key: 'community', label: 'Community', href: '/community' },
{ key: 'studio', label: 'Studio', href: '/studio' },
{ key: 'contact', label: 'Contact', href: '/contact' },
] as const;
---

<nav class="topbar wrap" data-nav aria-label="Main navigation">
<a href="/" class="topbar__logo" aria-label="channel47 home">
<LogoAnimated />
</a>
<div class="topbar__links">
{links.map((l) => (
<header class="topbar-shell" data-nav>
<nav class="topbar wrap" aria-label="Main navigation">
<a href="/" class="topbar__logo" aria-label="channel47 home">
<LogoAnimated />
</a>
<div class="topbar__links">
{links.map((l) => (
<a
href={l.href}
class={`topbar__link ${active === l.key ? 'is-active' : ''}`}
aria-current={active === l.key ? 'page' : undefined}
>{l.label}</a>
))}
<a
href={l.href}
class={`topbar__link ${active === l.key ? 'is-active' : ''}`}
aria-current={active === l.key ? 'page' : undefined}
>{l.label}</a>
))}
<a
href="/subscribe"
class={`topbar__link topbar__link--amber ${active === 'subscribe' ? 'is-active' : ''}`}
aria-current={active === 'subscribe' ? 'page' : undefined}
>Subscribe</a>
</div>
</nav>
href="/subscribe"
class={`topbar__link topbar__link--amber ${active === 'subscribe' ? 'is-active' : ''}`}
aria-current={active === 'subscribe' ? 'page' : undefined}
>Subscribe</a>
</div>
</nav>
</header>

<style>
.topbar-shell {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 50;
background: rgba(10, 10, 10, 0.72);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--color-line);
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32px clamp(24px, 5vw, 56px) 0;
height: var(--nav-h);
padding: 0 clamp(24px, 5vw, 56px);
}
.topbar__logo {
display: inline-flex;
Expand All @@ -58,6 +72,7 @@ const links = [
}
.topbar__link {
color: var(--color-text-dim);
padding-block: 4px;
transition: color var(--dur-instant);
}
.topbar__link:hover { color: var(--color-text); }
Expand All @@ -70,10 +85,10 @@ const links = [
.topbar__link--amber:hover { color: var(--color-amber); }

@media (max-width: 640px) {
.topbar { padding-top: 20px; gap: 16px; }
.topbar__links { gap: 16px; font-size: 12px; }
}
@media (max-width: 480px) {
.topbar { flex-direction: column; align-items: flex-start; }
.topbar__links { gap: 12px; font-size: 11px; }
.topbar__logo :global(.logo-animated) { font-size: 16px; }
}
</style>
19 changes: 19 additions & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Centralized external links + shared site data.
* Single source of truth so links never drift across pages.
*/

// Skool community join link — tracks signups via the affiliate ref.
// ⚠️ REPLACE `YOUR_AFFILIATE_REF` with your official Skool affiliate ref
// before launch, or signups will not be attributed to you.
export const SKOOL_URL =
'https://www.skool.com/the-vibe-marketers?ref=YOUR_AFFILIATE_REF';

// One-line install for the Field Kit plugin.
export const PLUGIN_INSTALL = 'claude plugin install channel47/field-kit';

export const SOCIALS = {
x: 'https://x.com/ctrlswing',
linkedin: 'https://linkedin.com/in/ctrlswing',
github: 'https://github.com/ctrlswing',
};
5 changes: 3 additions & 2 deletions src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const ogImage = Astro.site
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#0A0A0A" />
<!-- Flag JS availability before paint so reveal styles never hide content without JS -->
<script is:inline>document.documentElement.classList.add('js');</script>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />

Expand Down Expand Up @@ -85,8 +87,7 @@ const ogImage = Astro.site
<Analytics />
</head>
<body>
<a href="#main-content" class="skip-link">Skip to content</a>
<div class="noise-overlay" aria-hidden="true"></div>
<a href="#main" class="skip-link">Skip to content</a>
<slot />

<script>
Expand Down
Loading