Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3,397 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pagecord

Publish your writing effortlessly. All you need is email.

https://pagecord.com

Development

Quick Start with Docker

The easiest way to get Pagecord running locally is with Docker. docker compose up is the Docker equivalent of bin/dev — it starts PostgreSQL, Redis, Memcached, the Rails server, and the Tailwind watcher all in one go.

First, install Docker Desktop and make sure it's running.

Then:

git clone https://github.com/lylo/pagecord.git
cd pagecord
docker compose up

This stays running in your terminal. Open a second terminal for running commands.

You can view the app at http://localhost:3000. You can view individual blogs on their respective subdomains, e.g. http://joel.localhost:3000. Note: Safari doesn't support *.localhost subdomains - use lvh.me instead (e.g. http://joel.lvh.me:3000).

After pulling changes

# Rebuild the image (needed when Gemfile or Dockerfile changes)
docker compose up --build

# Clear stale CSS (needed when Tailwind output looks wrong or out of date)
docker compose exec web rake assets:clobber

Your local files are mounted into the container, so changes to .erb views and Ruby files take effect immediately. But compiled assets like app/assets/builds/tailwind.css can become stale — if the UI looks wrong after pulling, clear them with assets:clobber and the Tailwind watcher will regenerate.

Running commands in Docker

With docker compose up running in one terminal, use a second terminal for one-off commands:

# Rails console
docker compose exec web bin/rails console

# Run tests
docker compose exec web bin/rails test
docker compose exec web bin/rails test:system

# Run migrations
docker compose exec web bin/rails db:migrate

# Process emails (debug)
docker compose exec web bash -c "DIR=tmp/emails rake email:load"

Native Development (Alternative)

If you prefer to run Rails natively without Docker:

Click to expand native setup instructions

Install Ruby 3.4.5+ using HomeBrew and rbenv.

bundle install
rails db:setup
brew install redis postgresql
bin/dev

Run tests:

bin/rails test
bin/rails test:system

Processing an email locally

Sometimes you'll need to debug emails. To do this, save the .eml file(s) to a folder such as tmp/emails.

You can then run the following command which will parse all the .eml files in that folder and create posts for the first user account in the seed data (joel@pagecord.com).

DIR=tmp/emails rake email:load

Testing billing locally

Billing runs on Paddle Billing. When a subscription is created or changed, Paddle sends webhooks to /billing/paddle_events, and those webhook handlers are what actually set the plan, price, billing dates and send emails. The paddle:simulate rake tasks replay those webhooks against your running dev server with a valid signature, so you can exercise the whole subscription lifecycle without driving the Paddle sandbox by hand.

This covers everything after Paddle. The outbound side (the change_plan controller calling Paddle to switch a plan) is a real API call, so verifying Paddle's own proration still needs the sandbox.

The dev server must be running (bin/dev). Then:

# List the ready-made scenarios and the raw fixtures
bin/rails "paddle:scenarios"

# Run a named scenario against a user (blog subdomain, id, or email)
bin/rails "paddle:flow[signup_supporter,joel]"
bin/rails "paddle:flow[upgrade_to_supporter,joel]"
bin/rails "paddle:flow[downgrade_from_supporter,joel]"
bin/rails "paddle:flow[cancel,joel]"

# Replay a single webhook fixture, optionally overriding the plan in custom_data
bin/rails "paddle:simulate[subscription.created,joel,supporter]"
bin/rails "paddle:simulate[transaction.completed.plan_change.supporter,joel]"

Each run prints the subscription state before and after, so watch that rather than the HTTP status (the endpoint always returns 200). Fixtures live in test/fixtures/billing/; target a non-default host with PADDLE_SIMULATE_URL.

Signature verification needs a shared webhook_secret_key. In development this falls back to a fixed value (config/paddle.yml) when PADDLE_SANDBOX_WEBHOOK_SECRET_KEY is unset, so both the server and the simulator sign with the same key. Restart the dev server after changing that config. Supporter welcome emails are sent with deliver_later, so to see one you need your local mail catcher and job worker running.

Open Graph Images

Pagecord can optionally generate dynamic Open Graph images for blog posts using a separate Cloudflare Worker service. This worker is currently closed source and not required to run Pagecord locally.

What it does: Generates social media preview images for posts without explicit OG images.

Local development: Pagecord gracefully falls back to standard behavior when the worker is not configured. Your local installation will work perfectly fine without it - posts will simply use their first image or no OG image, just like they did originally.

If you're interested in setting up your own OG image worker, you can configure it with these optional environment variables:

OG_WORKER_URL=https://your-worker-url.com/og
OG_SIGNING_SECRET=your-secret-key

Log Analysis

Rake tasks for analysing production logs. They read log/production.log* in the repo, so they run the same on the server or locally against logs copied down (see below). No external gems required.

# Per-hour request overview — highlights anomalous traffic spikes
rake logs:overview

# Full report for a day (5 tables: requests/hour, endpoints, IPs, user agents, hosts)
rake "logs:report[2026-02-23]"

# Drill into a specific hour (requests/minute instead of per-hour)
rake "logs:report[2026-02-23,21]"

# Performance report for a day (response times, ActiveRecord time, query counts)
rake "logs:performance[2026-02-23]"

# Performance report for a specific hour
rake "logs:performance[2026-02-23,21]"

# Performance report for one blog or custom domain
HOST=joel rake "logs:performance[2026-02-23]"
HOST=example.com rake "logs:performance[2026-02-23,21]"

# AI bot robots.txt compliance — disallowed crawlers still hitting the site
rake logs:bots              # all retained logs
rake "logs:bots[2026-02-23]"  # a single day

# Live tail with per-minute request counter (alerts at >500 req/min)
rake logs:watch

# Traffic report for a specific blog (all available logs)
rake "logs:blog[joel]"

# Scoped to a specific date
rake "logs:blog[joel,2026-02-23]"

# Scoped to a specific hour
rake "logs:blog[joel,2026-02-23,21]"

# Works with custom domains too
rake "logs:blog[example.com]"

Running locally

Copy the production logs into the repo's log/ directory, then run any of the tasks above:

scp 'pagecord:pagecord/current/log/production.log*' log/

The parser reads every production.log* file, including rotated .gz, so this gives you the full retained window (a couple of weeks). Rotation maps as production.log (today), production.log.1.gz (yesterday), and so on, so grab a single file if you only need one day.

More info

Read about the Pagecord architecture or making contributions.

Follow the Pagecord blog.

Buy Me A Coffee

About

Blogging as easy as sending an email.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages