Skip to content

sveneisenschmidt/pop

Repository files navigation

Pop

Lightweight, self-hosted emoji reaction widget for blogs.

Pop Demo

Live Demo


Features

  • Emoji reactions with toggle support
  • Visitor counter with deduplication
  • Silent mode (no UI, only data collection)
  • Analytics dashboard
  • Fully customizable styling via CSS
  • Self-hosted with SQLite storage
  • Cookie-less & privacy-friendly
  • ~3KB minified frontend

Tech Stack

Frontend

  • TypeScript
  • esbuild
  • Vitest

Backend

  • PHP 8.4
  • Symfony 8
  • SQLite

Requirements

  • Node.js 20+
  • PHP 8.4+
  • Composer

Development

make install   # Install dependencies
make test      # Run tests
make dev       # Start dev server with file watching

Opens http://localhost:8000/demo.html

Deployment

GitHub Actions (Recommended)

The project includes GitHub Actions workflows for CI/CD:

  • Tests: Run automatically on push to main
  • Deploy: Manual trigger via GitHub Actions UI

Required secrets:

  • DEPLOY_SSH_KEY - Private SSH key
  • DEPLOY_HOST - Server hostname/IP
  • DEPLOY_USER - SSH username
  • DEPLOY_PATH - Target directory on server

Manual Deployment

  1. Build the project:

    make build
  2. Deploy these directories to your server:

    bin/        # Symfony console
    config/     # Symfony config
    public/     # Document root (index.php, pop.min.js, pop.min.css)
    src/        # PHP source
    vendor/     # PHP dependencies
    
  3. Create .env.local on the server:

    APP_ENV=prod
    APP_SECRET=<random-string>
    POP_ALLOWED_DOMAINS=https://your-blog.com
    POP_DATABASE_PATH=var/data.db
  4. Ensure var/ directory exists and is writable by the web server.

Docker Compose

services:
  pop:
    image: php:8.4-apache
    ports:
      - "8080:80"
    volumes:
      - ./:/var/www/html
      - pop-data:/var/www/html/var
    environment:
      - APP_ENV=prod
      - APP_SECRET=change-me-to-random-string
      - POP_ALLOWED_DOMAINS=https://your-blog.com
      - POP_DATABASE_PATH=var/data.db
    command: >
      bash -c "a2enmod rewrite &&
               echo '<Directory /var/www/html/public>
                 AllowOverride All
                 Require all granted
               </Directory>' > /etc/apache2/conf-available/pop.conf &&
               a2enconf pop &&
               sed -i 's|/var/www/html|/var/www/html/public|g' /etc/apache2/sites-available/000-default.conf &&
               apache2-foreground"

volumes:
  pop-data:

Usage

Full Widget (Reactions + Visitor Counter)

<link rel="stylesheet" href="https://your-api.com/pop.min.css">
<div id="pop"></div>
<script src="https://your-api.com/pop.min.js"></script>
<script>
  Pop.init({
    el: '#pop',
    endpoint: 'https://your-api.com/api',
    pageId: 'unique-page-id',  // optional, defaults to current URL
    emojis: ['πŸ‘', 'πŸ”₯', 'πŸ’‘', '❀️'],
    trackVisits: true,
    renderVisits: true,
    renderReactions: true
  });
</script>

Reactions Only (No Visitor Counter)

<div id="pop"></div>
<script src="https://your-api.com/pop.min.js"></script>
<script>
  Pop.init({
    el: '#pop',
    endpoint: 'https://your-api.com/api',
    emojis: ['πŸ‘', 'πŸ”₯', 'πŸ’‘', '❀️'],
    trackVisits: true,
    renderReactions: true
  });
</script>

Visitor Counter Only (No Reactions)

<div id="pop"></div>
<script src="https://your-api.com/pop.min.js"></script>
<script>
  Pop.init({
    el: '#pop',
    endpoint: 'https://your-api.com/api',
    trackVisits: true,
    renderVisits: true
  });
</script>

Silent Mode (Data Collection Only, No UI)

For recording visits without displaying anything:

<script src="https://your-api.com/pop.min.js"></script>
<script>
  Pop.init({
    endpoint: 'https://your-api.com/api',
    trackVisits: true
  });
</script>

Configuration Options

Option Type Default Description
endpoint string required API endpoint URL
pageId string current URL Unique identifier for the page
el string - CSS selector for container (required if rendering)
emojis string[] [] Array of emoji (required if renderReactions)
trackVisits boolean false Record visits to the server
renderVisits boolean false Show visitor counter (requires el)
renderReactions boolean false Show reaction buttons (requires el + emojis)
onLoad function - Callback when Pop is loaded

onLoad Callback

The onLoad callback is called after Pop has finished initializing. Use it to fetch additional data:

<script>
  Pop.init({
    el: '#pop',
    endpoint: 'https://your-api.com/api',
    emojis: ['πŸ‘', 'πŸ”₯'],
    renderReactions: true,
    trackVisits: true,
    onLoad: function() {
      fetch('https://your-api.com/api/visits?pageId=my-page')
        .then(res => res.json())
        .then(data => {
          console.log('Unique visitors:', data.uniqueVisitors);
          console.log('Total visits:', data.totalVisits);
        });
    }
  });
</script>

API

GET /api/reactions?pageId={id}

Returns reaction counts and current user's reactions.

{
  "pageId": "my-page",
  "reactions": { "πŸ‘": 5, "πŸ”₯": 3 },
  "userReactions": ["πŸ‘"]
}

POST /api/reactions

Toggles a reaction (adds if not present, removes if already added).

Request:

{ "pageId": "my-page", "emoji": "πŸ‘" }

Response:

{ "success": true, "action": "added", "count": 6 }

GET /api/visits?pageId={id}

Returns visitor counts for a page.

{
  "pageId": "my-page",
  "uniqueVisitors": 42,
  "totalVisits": 128
}

POST /api/visits

Records a visit. Deduplicates by fingerprint within a configurable window (default: 30 minutes).

Request:

{ "pageId": "my-page" }

Response:

{ "success": true, "recorded": true, "uniqueVisitors": 43 }

GET /api/stats

Returns aggregated statistics for all pages (used by analytics dashboard).

Query parameters:

  • pageIdFilter - filters pages containing the given string
  • group - group results by time period: day, week, or month
  • limit - number of periods to return (max: day=28, week=52, month=12)

Default response (no grouping):

{
  "global": {
    "uniqueVisitors": 1234,
    "totalVisits": 5678,
    "totalPages": 42
  },
  "pages": [
    { "pageId": "page-1", "uniqueVisitors": 100, "totalVisits": 250 },
    { "pageId": "page-2", "uniqueVisitors": 80, "totalVisits": 180 }
  ]
}

Grouped response (?group=day&limit=7):

{
  "day_0": {
    "global": { "uniqueVisitors": 50, "totalVisits": 120, "totalPages": 5 },
    "pages": [{ "pageId": "page-1", "uniqueVisitors": 30, "totalVisits": 80 }]
  },
  "day_1": {
    "global": { "uniqueVisitors": 45, "totalVisits": 100, "totalPages": 4 },
    "pages": [...]
  }
}

Where day_0 is today, day_1 is yesterday, etc. Same logic applies for week_0/week_1 and month_0/month_1.

Environment Variables

Variable Description
APP_SECRET Symfony secret key
POP_ALLOWED_DOMAINS Comma-separated allowed origins
POP_DATABASE_PATH Path to SQLite database file
POP_VISIT_DEDUP_SECONDS Visit deduplication window in seconds (default: 1800)

Analytics

Access the analytics dashboard at /analytics.html. Enter your API endpoint to view visitor statistics across all pages.

License

MIT

About

Lightweight, self-hosted and cookie-free emoji reaction widget for blogs.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors