Skip to content

Commit 7094c05

Browse files
committed
v2: Rewrite from Gatsby to Astro 5
Gatsby hadn't been touched in three years. The site still worked (still #1 on Google for "code smells catalog") but the framework was dead and the DX was worse. Rewrote the whole thing. Framework: Gatsby + React β†’ Astro 5 static output + Preact islands Styling: custom CSS β†’ Tailwind CSS v4 State: Redux β†’ Nano Stores Packages: npm β†’ pnpm Testing: nothing β†’ Vitest (unit + integration) + Playwright (E2E) CI/CD: β†’ GitHub Actions pipeline + Vercel preview deploys New pages: - About page with research context and taxonomy explorer - 404 with fuzzy matching - RSS feed, web manifest, sitemap - JSON-LD, OG image generation, canonical URLs The 56 smell articles are unchanged. Frontmatter got a Zod schema, code examples got Shiki dual-theme highlighting, internal links between smells resolve through a remark plugin now. Zero framework JS on most pages. Preact hydrates two things: the catalog filter sidebar and the code example toggle. Rest is inline scripts on Nano Stores. Old luzkan.github.io/smells URLs still redirect so bookmarks work. 381 files changed. Gatsby, Redux, and the old workflows are deleted.
1 parent 940047c commit 7094c05

381 files changed

Lines changed: 35223 additions & 50125 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
---
2+
description: Astro architecture, component patterns, island strategy, styling approach
3+
globs: src/**,astro.config.mjs
4+
alwaysApply: false
5+
---
6+
7+
# Architecture
8+
9+
## Stack
10+
11+
| Layer | Technology | Version | Notes |
12+
|-------|-----------|---------|-------|
13+
| Framework | Astro | 5.x | Content collections via glob loader |
14+
| Islands | Preact | 10.x | Via `@astrojs/preact`. Only FilterSidebar and CodeExample |
15+
| Shared State | Nano Stores | latest | `@nanostores/preact` for islands, vanilla subscriber for static HTML |
16+
| Styling | Tailwind CSS | 4.x | Via `@tailwindcss/vite` plugin (not `@astrojs/tailwind`) |
17+
| Fonts | Fontsource | latest | Self-hosted Fraunces, Plus Jakarta Sans, JetBrains Mono |
18+
| Syntax Highlighting | Shiki | built-in | Dual-theme: github-light / github-dark |
19+
| View Transitions | Astro `<ClientRouter />` | β€” | Crossfade default |
20+
| Testing | Vitest + Playwright | latest | Unit, integration, E2E (6-browser matrix on main) |
21+
22+
## Content Collections
23+
24+
Defined in `src/content.config.ts` using Astro 5 glob loader:
25+
26+
```ts
27+
defineCollection({
28+
loader: glob({ pattern: '**/*.md', base: './content/smells' }),
29+
schema: smellSchema,
30+
})
31+
```
32+
33+
`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.
34+
35+
The `---` null placeholder in YAML arrays is handled via Zod `.transform(arr => arr.filter(v => v !== '---'))` in `src/schemas/smell.ts`.
36+
37+
## Routing
38+
39+
| Route | File | Description |
40+
|-------|------|-------------|
41+
| `/` | `src/pages/index.astro` | Catalog with hero, filter sidebar, card grid |
42+
| `/smells/[slug]` | `src/pages/smells/[slug].astro` | Individual smell article (56 pages) |
43+
| `/about` | `src/pages/about.astro` | About page |
44+
| `/rss.xml` | `src/pages/rss.xml.ts` | RSS feed |
45+
| Custom 404 | `src/pages/404.astro` | 404 with fuzzy matching and diagnoses |
46+
47+
Uses `[slug].astro` (not `[...slug].astro`) because all 56 slugs are flat β€” rest params would let `/smells/foo/bar` match instead of 404ing.
48+
49+
## Internal Links
50+
51+
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.
52+
53+
## Remark Plugin Pipeline
54+
55+
Configured in `astro.config.mjs`. Ordering is load-bearing:
56+
57+
1. `remark-smell-links` β€” rewrite `./slug.md` links while headings are still original
58+
2. `remark-overview-heading` β€” normalize first H2 to "Overview"
59+
3. `remark-strip-sections` β€” strip sections rendered by Astro components (Problems, Example, Refactoring, Sources)
60+
4. `remark-callout-sections` β€” wrap remaining callout sections
61+
62+
## Islands Architecture
63+
64+
Most pages ship **zero framework JavaScript**. Preact is used only for two interactive components:
65+
66+
| Component | Hydration | Location |
67+
|-----------|-----------|----------|
68+
| FilterSidebar | `client:load` | Catalog β€” includes search input, dimension filters, active pills, mobile bottom sheet |
69+
| CodeExample | `client:visible` | Article pages β€” smelly/solution code toggle with copy button |
70+
71+
**Everything else uses inline `<script>` tags** in Astro components:
72+
- ThemeToggle β€” DOM read + CSS write
73+
- Nav scroll behavior and active link tracking
74+
- ToC scroll spy β€” IntersectionObserver
75+
- Card visibility β€” Nano Store subscriber toggling CSS classes
76+
- Catalog toolbar (view toggle, sort)
77+
- Footer citation copy, 404 fuzzy search, analytics consent
78+
79+
### Nano Stores
80+
81+
FilterSidebar (Preact) must show/hide static HTML cards. Nano Stores (~1KB, framework-agnostic) bridges the gap:
82+
- Filter state lives in stores (`src/stores/filters.ts`, `src/stores/search.ts`)
83+
- Derived stores compute filtered results (`src/stores/derived/filtered-smells.ts`, `filter-counts.ts`, `active-count.ts`)
84+
- FilterSidebar writes to the stores
85+
- Inline `<script>` on the catalog page subscribes and toggles CSS classes on cards
86+
87+
### Error Boundaries
88+
89+
Every Preact island has an error boundary with graceful degradation:
90+
- FilterSidebar error -> show all cards unfiltered
91+
- CodeExample error -> show "Smelly" panel as static HTML
92+
- Site must work without JS
93+
94+
## Theme
95+
96+
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.
97+
98+
## View Transitions
99+
100+
Uses `astro:page-load` and `astro:before-swap` events for lifecycle management. Components register via `src/lib/lifecycle.ts`:
101+
- `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`.
102+
- `initAll(selector, setup)` β€” same pattern but for `querySelectorAll` (multiple roots).
103+
104+
## What NOT to Add
105+
106+
- No Redux / global state library β€” Nano Stores only
107+
- No GraphQL β€” Astro content collections provide typed data directly
108+
- No MUI / component library β€” Tailwind + custom components
109+
- No Partytown β€” GA4 reliability issues not worth the complexity for one script
110+
- No Pagefind / server-side search β€” 56 items searched client-side
111+
- No service worker β€” the old one caused caching headaches; kill switch deployed
112+
- No `@astrojs/vercel` adapter β€” static output only, Vercel auto-detects Astro
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
---
2+
description: Full frontmatter schema and taxonomy for code smell content files
3+
globs: content/**
4+
alwaysApply: false
5+
---
6+
7+
# Content Model β€” Code Smell Articles
8+
9+
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).
10+
11+
## Frontmatter Schema
12+
13+
```yaml
14+
slug: "kebab-case-name" # matches filename, used as URL path
15+
meta:
16+
last_update_date: 2022-04-19 # YYYY-MM-DD format
17+
title: "Human Readable Name"
18+
description: "Optional short authored blurb" # used by hero/cards/SEO/RSS; falls back to body if absent
19+
cover: "/path/to/image.png" # optional, not yet used β€” schema accepts it for future use
20+
known_as: # aliases, or ["---"] if none
21+
- "Alternate Name"
22+
categories:
23+
expanse: "Within" # exactly one of: Within | Between
24+
obstruction: # one or more from Obstruction enum
25+
- "Bloaters"
26+
occurrence: # one or more from Occurrence enum
27+
- "Names"
28+
tags: # Major | Minor, or ["---"] if untagged
29+
- "---"
30+
smell_hierarchies: # one or more from SmellHierarchy enum
31+
- "Code Smell"
32+
- "Implementation Smell"
33+
relations:
34+
related_smells:
35+
- name: "Human Readable Name"
36+
slug: kebab-case-name
37+
type: # one or more of: causes | caused | co-exist | family | antagonistic
38+
- "causes"
39+
problems:
40+
general: # string array, or ["---"] if none
41+
- "Readability"
42+
violation:
43+
principles: # string array, or ["---"] if none
44+
- "Single Responsibility"
45+
patterns: # string array, or ["---"] if none
46+
- "---"
47+
refactors: # string array
48+
- "Extract Method"
49+
- "Replace with Symbolic Constant"
50+
history:
51+
- author: "Martin Fowler"
52+
type: "origin" # origin | parentage | mention | update
53+
named_as:
54+
- "Duplicated Code"
55+
regarded_as:
56+
- "Code Smell"
57+
source:
58+
year: 1999
59+
authors:
60+
- "Martin Fowler"
61+
name: "Refactoring: Improving the Design of Existing Code"
62+
type: "book" # book | thesis | paper | course | cheatsheet
63+
href:
64+
isbn_13: "978-0201485677" # optional
65+
isbn_10: "0201485672" # optional
66+
direct_url: "https://..." # optional
67+
journal: "..." # optional (for articles)
68+
pages: "..." # optional
69+
publisher: "..." # optional
70+
volume: "..." # optional
71+
number: "..." # optional
72+
```
73+
74+
## Taxonomy Enums (valid values)
75+
76+
### Expanse
77+
- Within
78+
- Between
79+
80+
### Obstruction
81+
- Bloaters
82+
- Change Preventers
83+
- Couplers
84+
- Data Dealers
85+
- Dispensables
86+
- Functional Abusers
87+
- Lexical Abusers
88+
- Obfuscators
89+
- Object Oriented Abusers
90+
- Other
91+
92+
### Occurrence
93+
- Duplication
94+
- Responsibility
95+
- Measured Smells
96+
- Data
97+
- Unnecessary Complexity
98+
- Interfaces
99+
- Names
100+
- Message Calls
101+
- Conditional Logic
102+
103+
### Smell Hierarchies
104+
- Antipattern
105+
- Architecture Smell
106+
- Code Smell
107+
- Design Smell
108+
- Implementation Smell
109+
- Linguistic Antipattern
110+
- Linguistic Smell
111+
112+
### Tags
113+
- Major
114+
- Minor
115+
116+
### Relation Types
117+
- **causes** β€” this smell leads to the related smell
118+
- **caused** β€” this smell is caused by the related smell
119+
- **co-exist** β€” these smells tend to appear together
120+
- **family** β€” these smells are conceptually related / variations of each other
121+
- **antagonistic** β€” these smells are opposites or mutually exclusive
122+
123+
## Conventions
124+
125+
- `---` is the null/empty placeholder in YAML arrays (not `null`, not omitted)
126+
- `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
127+
- Internal links between smells use relative markdown links: `[Name](./slug.md)`
128+
- These links are transformed at build time to proper URLs by a remark plugin
129+
- `problems` are rendered via `src/components/article/ProblemCards.astro`; `refactors` via `src/components/article/RefactoringList.astro`
130+
131+
## Body Structure
132+
133+
```markdown
134+
## Smell Title
135+
136+
Prose explanation of the smell...
137+
138+
### Problems:
139+
140+
#### Problem Name
141+
142+
Explanation...
143+
144+
### Example
145+
146+
<div class="example-block">
147+
148+
#### Smelly:
149+
150+
Description of the bad pattern
151+
152+
\`\`\`language
153+
// bad code
154+
\`\`\`
155+
156+
#### Solution:
157+
158+
Description of the fix
159+
160+
\`\`\`language
161+
// good code
162+
\`\`\`
163+
164+
</div>
165+
166+
### Refactoring:
167+
168+
- Technique 1
169+
- Technique 2
170+
171+
---
172+
173+
##### Sources
174+
175+
- [[1](#sources)] - Author, "Title" (Year)
176+
```
177+
178+
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.

β€Ž.cursor/rules/deployment.mdcβ€Ž

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
description: CI/CD pipelines, Vercel deployment, GitHub Pages redirects, SEO preservation
3+
globs: .github/workflows/**,vercel.json,scripts/**,public/sw.js
4+
alwaysApply: false
5+
---
6+
7+
# Deployment
8+
9+
## Production (Vercel)
10+
11+
- **Domain**: codesmells.org
12+
- **Output**: Static (`output: 'static'` in `astro.config.mjs`), no `@astrojs/vercel` adapter
13+
- **Build**: `pnpm build` (runs `scripts/generate-og-images.mjs` then `astro build`)
14+
- **Config**: `vercel.json` β€” security headers, cache rules for `/_astro/*` and `/og/*`, legacy `/smells/*` redirects (301)
15+
- **Preview deploys**: Automatic on PRs via Vercel GitHub integration
16+
- **Trailing slash**: `never` in both `astro.config.mjs` and `vercel.json`
17+
18+
## Environment Variables
19+
20+
| Variable | Where | Notes |
21+
|----------|-------|-------|
22+
| `PUBLIC_GA_TRACKING_ID` | Vercel (Production only) | GA4 measurement ID, consent-gated |
23+
24+
Site URL is hardcoded in `src/lib/constants.ts` (`https://codesmells.org`), not an env var.
25+
26+
## CI Pipeline (`.github/workflows/ci.yml`)
27+
28+
Runs on push to `main` and PRs:
29+
30+
1. **Lint** β€” ESLint + Prettier + `astro check` (type-check)
31+
2. **Unit Tests** β€” Vitest (`pnpm test`)
32+
3. **Build** β€” `pnpm build`, verifies 56 smell pages in `dist/smells`, validates RSS feed, runs `scripts/verify-sitemap.mjs`, runs `pnpm test:build-output`
33+
4. **Bundle Size** β€” `size-limit` against build output (configured in `.size-limit.json`)
34+
5. **E2E** β€” Playwright. PRs: chromium + reduced-motion. Main: 6-browser matrix (chromium, reduced-motion, firefox, webkit, mobile-chrome, mobile-safari)
35+
36+
Node 22, pnpm 10. Build artifact passed between jobs.
37+
38+
## Preview E2E (`.github/workflows/e2e-preview.yml`)
39+
40+
Triggers on `deployment_status` for Vercel preview environments. Runs Playwright smoke tests (`@smoke` tag) against the preview URL.
41+
42+
## GitHub Pages (Redirect Layer)
43+
44+
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.
45+
46+
## SEO Assets
47+
48+
| Asset | Implementation |
49+
|-------|---------------|
50+
| Sitemap | `@astrojs/sitemap` integration |
51+
| RSS | `@astrojs/rss` at `src/pages/rss.xml.ts` |
52+
| Canonical URLs | `<link rel="canonical">` in `SEOHead.astro` pointing to `codesmells.org` |
53+
| Meta tags | `src/components/seo/SEOHead.astro` (title, description, OpenGraph, Twitter) |
54+
| Structured data | `src/components/seo/JsonLd.astro` (WebSite, Article, BreadcrumbList, ItemList) |
55+
| OG images | Pre-generated locally via `scripts/generate-og-images.mjs`, committed as static assets |
56+
| Robots | `public/robots.txt` |
57+
| Manifest | `src/pages/site.webmanifest.ts` (generated from content collection count) |
58+
| Analytics | GA4 via `PUBLIC_GA_TRACKING_ID` (consent-gated) + `@vercel/analytics` |
59+
60+
## Anti-patterns
61+
62+
- Do not add `@astrojs/vercel` β€” static output needs no adapter
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
description: Core project identity and orientation for every agent session
3+
globs:
4+
alwaysApply: true
5+
---
6+
7+
# Code Smells Catalog β€” Project Context
8+
9+
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.
10+
11+
- Live at `https://codesmells.org` (Vercel) β€” **#1 Google result** for "code smells catalog"
12+
- Legacy URL `https://luzkan.github.io/smells` redirects to codesmells.org
13+
- Content lives in `content/smells/*.md` (56 files with rich YAML frontmatter) β€” this is the primary asset
14+
- Built with **Astro 5**, Preact islands, Tailwind CSS v4, Nano Stores
15+
- Deployed to **Vercel** as static output (no adapter)
16+
17+
## Critical constraints
18+
19+
- **Do not modify** the content frontmatter schema without explicit instruction
20+
- **SEO is paramount** β€” preserve URL structure, canonical URLs, meta tags, sitemap, and redirects
21+
- The Python `data_scraper/` reads the same markdown files β€” keep it working
22+
- Internal links between smells use `./slug.md` β€” transformed by `src/plugins/remark-smell-links.ts`
23+
24+
## Key directories
25+
26+
- `content/smells/` β€” the 56 smell articles (Markdown + YAML frontmatter)
27+
- `src/` β€” Astro application (components, layouts, pages, lib, stores, plugins)
28+
- `scripts/` β€” build helpers (OG images, GH Pages redirects, sitemap verification)
29+
- `tests/` β€” unit (Vitest), integration, E2E (Playwright)
30+
- `.cursor/rules/` β€” agent context rules (content-model, architecture, deployment)
31+
32+
For the full frontmatter schema, see `content-model.mdc`. For architecture details, see `architecture.mdc`. For deployment, see `deployment.mdc`.

β€Ž.env.exampleβ€Ž

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copy to .env and fill in values
2+
# shellcheck shell=sh
3+
# shellcheck disable=SC2034
4+
5+
# Google Analytics 4 measurement ID (consent-gated; leave empty to disable)
6+
PUBLIC_GA_TRACKING_ID=
7+
8+
# --- Vercel-injected (do not set locally) ---
9+
# VERCEL_ENV β€” "production" | "preview" | "development"
10+
# Auto-injected by Vercel at build/runtime.
11+
# Used to gate Vercel Web Analytics (production only).

0 commit comments

Comments
Β (0)