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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
112 changes: 112 additions & 0 deletions .cursor/rules/architecture.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
description: Astro architecture, component patterns, island strategy, styling approach
globs: src/**,astro.config.mjs
alwaysApply: false
---

# Architecture

## Stack

| Layer | Technology | Version | Notes |
|-------|-----------|---------|-------|
| Framework | Astro | 5.x | Content collections via glob loader |
| Islands | Preact | 10.x | Via `@astrojs/preact`. Only FilterSidebar and CodeExample |
| Shared State | Nano Stores | latest | `@nanostores/preact` for islands, vanilla subscriber for static HTML |
| Styling | Tailwind CSS | 4.x | Via `@tailwindcss/vite` plugin (not `@astrojs/tailwind`) |
| Fonts | Fontsource | latest | Self-hosted Fraunces, Plus Jakarta Sans, JetBrains Mono |
| Syntax Highlighting | Shiki | built-in | Dual-theme: github-light / github-dark |
| View Transitions | Astro `<ClientRouter />` | — | Crossfade default |
| Testing | Vitest + Playwright | latest | Unit, integration, E2E (6-browser matrix on main) |

## Content Collections

Defined in `src/content.config.ts` using Astro 5 glob loader:

```ts
defineCollection({
loader: glob({ pattern: '**/*.md', base: './content/smells' }),
schema: smellSchema,
})
```

`base` resolves relative to the project root (where `astro.config.mjs` lives), NOT relative to `src/content.config.ts`. Content files stay at `content/smells/` — no move, no symlinks.

The `---` null placeholder in YAML arrays is handled via Zod `.transform(arr => arr.filter(v => v !== '---'))` in `src/schemas/smell.ts`.

## Routing

| Route | File | Description |
|-------|------|-------------|
| `/` | `src/pages/index.astro` | Catalog with hero, filter sidebar, card grid |
| `/smells/[slug]` | `src/pages/smells/[slug].astro` | Individual smell article (56 pages) |
| `/about` | `src/pages/about.astro` | About page |
| `/rss.xml` | `src/pages/rss.xml.ts` | RSS feed |
| Custom 404 | `src/pages/404.astro` | 404 with fuzzy matching and diagnoses |

Uses `[slug].astro` (not `[...slug].astro`) because all 56 slugs are flat — rest params would let `/smells/foo/bar` match instead of 404ing.

## Internal Links

Remark plugin `src/plugins/remark-smell-links.ts` rewrites `[Name](./slug.md)` to `<a href="/smells/slug">Name</a>` at build time. Keeps content files portable.

## Remark Plugin Pipeline

Configured in `astro.config.mjs`. Ordering is load-bearing:

1. `remark-smell-links` — rewrite `./slug.md` links while headings are still original
2. `remark-overview-heading` — normalize first H2 to "Overview"
3. `remark-strip-sections` — strip sections rendered by Astro components (Problems, Example, Refactoring, Sources)
4. `remark-callout-sections` — wrap remaining callout sections

## Islands Architecture

Most pages ship **zero framework JavaScript**. Preact is used only for two interactive components:

| Component | Hydration | Location |
|-----------|-----------|----------|
| FilterSidebar | `client:load` | Catalog — includes search input, dimension filters, active pills, mobile bottom sheet |
| CodeExample | `client:visible` | Article pages — smelly/solution code toggle with copy button |

**Everything else uses inline `<script>` tags** in Astro components:
- ThemeToggle — DOM read + CSS write
- Nav scroll behavior and active link tracking
- ToC scroll spy — IntersectionObserver
- Card visibility — Nano Store subscriber toggling CSS classes
- Catalog toolbar (view toggle, sort)
- Footer citation copy, 404 fuzzy search, analytics consent

### Nano Stores

FilterSidebar (Preact) must show/hide static HTML cards. Nano Stores (~1KB, framework-agnostic) bridges the gap:
- Filter state lives in stores (`src/stores/filters.ts`, `src/stores/search.ts`)
- Derived stores compute filtered results (`src/stores/derived/filtered-smells.ts`, `filter-counts.ts`, `active-count.ts`)
- FilterSidebar writes to the stores
- Inline `<script>` on the catalog page subscribes and toggles CSS classes on cards

### Error Boundaries

Every Preact island has an error boundary with graceful degradation:
- FilterSidebar error -> show all cards unfiltered
- CodeExample error -> show "Smelly" panel as static HTML
- Site must work without JS

## Theme

Blocking inline `<script>` in `<head>` reads `localStorage` / `prefers-color-scheme` and sets `data-theme` before first paint. Prevents flash. Theme toggle is an inline script, not Preact.

## View Transitions

Uses `astro:page-load` and `astro:before-swap` events for lifecycle management. Components register via `src/lib/lifecycle.ts`:
- `initOnce(selector, setup)` — finds the first element matching `selector`, runs `setup` once per DOM node (WeakSet dedup). If `setup` returns a cleanup function, it runs on `astro:before-swap`. Re-runs on each `astro:page-load`.
- `initAll(selector, setup)` — same pattern but for `querySelectorAll` (multiple roots).

## What NOT to Add

- No Redux / global state library — Nano Stores only
- No GraphQL — Astro content collections provide typed data directly
- No MUI / component library — Tailwind + custom components
- No Partytown — GA4 reliability issues not worth the complexity for one script
- No Pagefind / server-side search — 56 items searched client-side
- No service worker — the old one caused caching headaches; kill switch deployed
- No `@astrojs/vercel` adapter — static output only, Vercel auto-detects Astro
178 changes: 178 additions & 0 deletions .cursor/rules/content-model.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
---
description: Full frontmatter schema and taxonomy for code smell content files
globs: content/**
alwaysApply: false
---

# Content Model — Code Smell Articles

Each smell is a Markdown file in `content/smells/` with YAML frontmatter followed by a structured body. There are 56 files total. The filename matches the `slug` field (kebab-case).

## Frontmatter Schema

```yaml
slug: "kebab-case-name" # matches filename, used as URL path
meta:
last_update_date: 2022-04-19 # YYYY-MM-DD format
title: "Human Readable Name"
description: "Optional short authored blurb" # used by hero/cards/SEO/RSS; falls back to body if absent
cover: "/path/to/image.png" # optional, not yet used — schema accepts it for future use
known_as: # aliases, or ["---"] if none
- "Alternate Name"
categories:
expanse: "Within" # exactly one of: Within | Between
obstruction: # one or more from Obstruction enum
- "Bloaters"
occurrence: # one or more from Occurrence enum
- "Names"
tags: # Major | Minor, or ["---"] if untagged
- "---"
smell_hierarchies: # one or more from SmellHierarchy enum
- "Code Smell"
- "Implementation Smell"
relations:
related_smells:
- name: "Human Readable Name"
slug: kebab-case-name
type: # one or more of: causes | caused | co-exist | family | antagonistic
- "causes"
problems:
general: # string array, or ["---"] if none
- "Readability"
violation:
principles: # string array, or ["---"] if none
- "Single Responsibility"
patterns: # string array, or ["---"] if none
- "---"
refactors: # string array
- "Extract Method"
- "Replace with Symbolic Constant"
history:
- author: "Martin Fowler"
type: "origin" # origin | parentage | mention | update
named_as:
- "Duplicated Code"
regarded_as:
- "Code Smell"
source:
year: 1999
authors:
- "Martin Fowler"
name: "Refactoring: Improving the Design of Existing Code"
type: "book" # book | thesis | paper | course | cheatsheet
href:
isbn_13: "978-0201485677" # optional
isbn_10: "0201485672" # optional
direct_url: "https://..." # optional
journal: "..." # optional (for articles)
pages: "..." # optional
publisher: "..." # optional
volume: "..." # optional
number: "..." # optional
```

## Taxonomy Enums (valid values)

### Expanse
- Within
- Between

### Obstruction
- Bloaters
- Change Preventers
- Couplers
- Data Dealers
- Dispensables
- Functional Abusers
- Lexical Abusers
- Obfuscators
- Object Oriented Abusers
- Other

### Occurrence
- Duplication
- Responsibility
- Measured Smells
- Data
- Unnecessary Complexity
- Interfaces
- Names
- Message Calls
- Conditional Logic

### Smell Hierarchies
- Antipattern
- Architecture Smell
- Code Smell
- Design Smell
- Implementation Smell
- Linguistic Antipattern
- Linguistic Smell

### Tags
- Major
- Minor

### Relation Types
- **causes** — this smell leads to the related smell
- **caused** — this smell is caused by the related smell
- **co-exist** — these smells tend to appear together
- **family** — these smells are conceptually related / variations of each other
- **antagonistic** — these smells are opposites or mutually exclusive

## Conventions

- `---` is the null/empty placeholder in YAML arrays (not `null`, not omitted)
- `meta.description` is optional authored copy for the article hero, catalog cards, structured data, and RSS; when missing, the site falls back to the first overview paragraph extracted from the body
- Internal links between smells use relative markdown links: `[Name](./slug.md)`
- These links are transformed at build time to proper URLs by a remark plugin
- `problems` are rendered via `src/components/article/ProblemCards.astro`; `refactors` via `src/components/article/RefactoringList.astro`

## Body Structure

```markdown
## Smell Title

Prose explanation of the smell...

### Problems:

#### Problem Name

Explanation...

### Example

<div class="example-block">

#### Smelly:

Description of the bad pattern

\`\`\`language
// bad code
\`\`\`

#### Solution:

Description of the fix

\`\`\`language
// good code
\`\`\`

</div>

### Refactoring:

- Technique 1
- Technique 2

---

##### Sources

- [[1](#sources)] - Author, "Title" (Year)
```

Not all articles follow this structure exactly — some are shorter (e.g., `duplicated-code.md` has minimal body). The frontmatter is consistent across all 56 files.
62 changes: 62 additions & 0 deletions .cursor/rules/deployment.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
description: CI/CD pipelines, Vercel deployment, GitHub Pages redirects, SEO preservation
globs: .github/workflows/**,vercel.json,scripts/**,public/sw.js
alwaysApply: false
---

# Deployment

## Production (Vercel)

- **Domain**: codesmells.org
- **Output**: Static (`output: 'static'` in `astro.config.mjs`), no `@astrojs/vercel` adapter
- **Build**: `pnpm build` (runs `scripts/generate-og-images.mjs` then `astro build`)
- **Config**: `vercel.json` — security headers, cache rules for `/_astro/*` and `/og/*`, legacy `/smells/*` redirects (301)
- **Preview deploys**: Automatic on PRs via Vercel GitHub integration
- **Trailing slash**: `never` in both `astro.config.mjs` and `vercel.json`

## Environment Variables

| Variable | Where | Notes |
|----------|-------|-------|
| `PUBLIC_GA_TRACKING_ID` | Vercel (Production only) | GA4 measurement ID, consent-gated |

Site URL is hardcoded in `src/lib/constants.ts` (`https://codesmells.org`), not an env var.

## CI Pipeline (`.github/workflows/ci.yml`)

Runs on push to `main` and PRs:

1. **Lint** — ESLint + Prettier + `astro check` (type-check)
2. **Unit Tests** — Vitest (`pnpm test`)
3. **Build** — `pnpm build`, verifies 56 smell pages in `dist/smells`, validates RSS feed, runs `scripts/verify-sitemap.mjs`, runs `pnpm test:build-output`
4. **Bundle Size** — `size-limit` against build output (configured in `.size-limit.json`)
5. **E2E** — Playwright. PRs: chromium + reduced-motion. Main: 6-browser matrix (chromium, reduced-motion, firefox, webkit, mobile-chrome, mobile-safari)

Node 22, pnpm 10. Build artifact passed between jobs.

## Preview E2E (`.github/workflows/e2e-preview.yml`)

Triggers on `deployment_status` for Vercel preview environments. Runs Playwright smoke tests (`@smoke` tag) against the preview URL.

## GitHub Pages (Redirect Layer)

The `gh-pages` branch serves HTML meta-refresh redirects from the legacy `luzkan.github.io/smells` URLs to `codesmells.org`. Generated by `scripts/generate-gh-pages-redirects.mjs`, deployed via manual workflows.

## SEO Assets

| Asset | Implementation |
|-------|---------------|
| Sitemap | `@astrojs/sitemap` integration |
| RSS | `@astrojs/rss` at `src/pages/rss.xml.ts` |
| Canonical URLs | `<link rel="canonical">` in `SEOHead.astro` pointing to `codesmells.org` |
| Meta tags | `src/components/seo/SEOHead.astro` (title, description, OpenGraph, Twitter) |
| Structured data | `src/components/seo/JsonLd.astro` (WebSite, Article, BreadcrumbList, ItemList) |
| OG images | Pre-generated locally via `scripts/generate-og-images.mjs`, committed as static assets |
| Robots | `public/robots.txt` |
| Manifest | `src/pages/site.webmanifest.ts` (generated from content collection count) |
| Analytics | GA4 via `PUBLIC_GA_TRACKING_ID` (consent-gated) + `@vercel/analytics` |

## Anti-patterns

- Do not add `@astrojs/vercel` — static output needs no adapter
32 changes: 32 additions & 0 deletions .cursor/rules/project-context.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
description: Core project identity and orientation for every agent session
globs:
alwaysApply: true
---

# Code Smells Catalog — Project Context

This is the **Code Smells Catalog** — a structured reference of 56 code smells, built by Marcel Jerzyk as a companion to his Springer Nature research paper and Master's thesis.

- Live at `https://codesmells.org` (Vercel) — **#1 Google result** for "code smells catalog"
- Legacy URL `https://luzkan.github.io/smells` redirects to codesmells.org
- Content lives in `content/smells/*.md` (56 files with rich YAML frontmatter) — this is the primary asset
- Built with **Astro 5**, Preact islands, Tailwind CSS v4, Nano Stores
- Deployed to **Vercel** as static output (no adapter)

## Critical constraints

- **Do not modify** the content frontmatter schema without explicit instruction
- **SEO is paramount** — preserve URL structure, canonical URLs, meta tags, sitemap, and redirects
- The Python `data_scraper/` reads the same markdown files — keep it working
- Internal links between smells use `./slug.md` — transformed by `src/plugins/remark-smell-links.ts`

## Key directories

- `content/smells/` — the 56 smell articles (Markdown + YAML frontmatter)
- `src/` — Astro application (components, layouts, pages, lib, stores, plugins)
- `scripts/` — build helpers (OG images, GH Pages redirects, sitemap verification)
- `tests/` — unit (Vitest), integration, E2E (Playwright)
- `.cursor/rules/` — agent context rules (content-model, architecture, deployment)

For the full frontmatter schema, see `content-model.mdc`. For architecture details, see `architecture.mdc`. For deployment, see `deployment.mdc`.
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copy to .env and fill in values
# shellcheck shell=sh
# shellcheck disable=SC2034

# Google Analytics 4 measurement ID (consent-gated; leave empty to disable)
PUBLIC_GA_TRACKING_ID=

# --- Vercel-injected (do not set locally) ---
# VERCEL_ENV — "production" | "preview" | "development"
# Auto-injected by Vercel at build/runtime.
# Used to gate Vercel Web Analytics (production only).
Loading
Loading