﻿# Daily Tarot (Sun and Moon Tarot Guide)

WordPress plugin for publishing a Card of the Day with a clean admin workflow, automation, packs/backup, and shortcodes.

- Product roadmap / definition of done: [Milestones.md](Milestones.md)
- Running changelog: [updates.md](updates.md)

Bootstraps from [daily-tarot.php](daily-tarot.php) and wires everything in [includes/Plugin.php](includes/Plugin.php).

## Free vs Pro

Free includes the full core workflow:

- Calendar (edit today + tomorrow)
- Decks and Meaning Packs
- Spreads (scan + presets + meaning packs; free users can create one custom meaning pack)
- Shortcodes + Gutenberg blocks
- Bookings (requests, reading types, availability, emails)
- Feedback (admin-only)

Pro unlocks:

- AI Integration (experimental) + provider settings
- Analytics dashboard
- Calendar editing beyond tomorrow
- Booking payment options (PayPal/Stripe + pay before/after)

## Requirements

- WordPress 6.0+
- PHP 8.1+

## SEO optimization

Daily Tarot includes built-in SEO helpers for the daily “readable” routes and card URLs.

- Readable routes: pretty, indexable URLs for daily pages and card pages (see `ReadableRoutes`).
- XML sitemap: publishes `dtarot-sitemap.xml` (optionally `dtarot-sitemap-index.xml`) for readable daily URLs.
- robots.txt: automatically appends the Daily Tarot sitemap URL to `robots.txt`.
- Yoast / Rank Math integration:
  - automatically appends Daily Tarot sitemap to the SEO plugin sitemap index
  - sets canonical + OpenGraph/Twitter metadata for readable routes
- JSON-LD schema: outputs Article JSON-LD on readable daily pages when no SEO plugin is active (configurable).

Notes:

- If you turn on readable routes or upgrade the plugin, re-save permalinks once (WP → Settings → Permalinks) to refresh rewrite rules.
- Sitemap URL is `https://yoursite.com/dtarot-sitemap.xml` by default.

## Development

### Translations (POT)

Generate/update the POT file (requires WP-CLI with `wp i18n make-pot` available):

- Windows: `powershell -ExecutionPolicy Bypass -File tools/make-pot.ps1`

### Useful filters

- `dtarot_analytics_enabled` (bool): disable GA4 `dataLayer` events
- `dtarot_readable_fallback_latest` (bool): disable redirect-to-latest when a requested day is missing
- `dtarot_schema_output_enabled` (bool): control JSON-LD output on readable routes
- `dtarot_sitemap_limit` (int): number of URLs in `dtarot-sitemap.xml`
- `dtarot_sitemap_noindex` (bool): add `X-Robots-Tag: noindex` to the sitemap response (default off)
- `dtarot_sitemap_cache_ttl` (int): sitemap cache TTL in seconds
- `dtarot_sitemap_use_index` (bool): when true, `Sitemap::url()` points to `/dtarot-sitemap-index.xml`
- `dtarot_sitemap_per_sitemap` (int): URLs per sitemap page when index mode is enabled
- `dtarot_sitemap_include_images` (bool): when true, outputs `<image:image>` entries
- `dtarot_sitemap_image_url` (string, date, entry|null): return an image URL for a given day
- `dtarot_robots_sitemap_enabled` (bool): include sitemap URL in `robots.txt`
- `dtarot_rate_limit_enabled` (bool, action): enable/disable rate limiting per endpoint
- `dtarot_rate_limit_key` (string, action): customize rate limit keying (IP/user-based)
- `dtarot_rate_limit_ip` (string, action): customize client IP detection (proxy support)
- `dtarot_rate_limit_identifier` (string, action): override the rate limit identifier completely
- `dtarot_ai_provider_allow_query_secret` (bool): allow `?secret=` fallback for provider endpoint (default false)
- `dtarot_log_max_entries` (int): cap stored log entries

Yoast/Rank Math: the plugin appends `dtarot-sitemap.xml` to the SEO plugin sitemap index automatically.

## Quick install

- Copy this plugin folder into `wp-content/plugins/`
- Activate Daily Tarot in WordPress Admin -> Plugins
- If the readable route is enabled/used, re-save permalinks once after activation (WP -> Settings -> Permalinks)

Troubleshooting:

- If card links like `/cards/tarot/tarot_major_1/` return a 404, re-save permalinks (WP -> Settings -> Permalinks -> Save Changes). This rebuilds rewrite rules. Plugin upgrades can require this too.

## Admin workflow (day-to-day)

Dashboard shortcuts:

- Daily Tarot -> Dashboard provides quick links, including an Edit today button that opens the Calendar modal directly for today.
- Dashboard shows at-a-glance cards for Today, upcoming readiness, and Automation.
- Daily Tarot -> Analytics focuses on unique visitors and trends (see Analytics below).

### 1) Create a Deck (images)

Go to Daily Tarot -> Content -> Decks.

- Create a new deck
- Set Deck Back (URL input + Choose + Save)
- Then go to Content -> Cards to upload per-card images for that deck

### 2) Create a Meaning Pack (meanings)

Go to Daily Tarot -> Content -> Meaning Packs.

- Create meanings per card
- If a day's manual text is empty on the frontend, meaning-pack text is used automatically

### 3) Publish the Calendar

Go to Daily Tarot -> Calendar.

- Pick deck + card + meaning pack
- Write Daily Text and Daily Text (expanded)
- Set status to `publish`

The calendar modal shows a ready indicator and prevents setting `publish` without selecting deck + card.

Quick navigation:

- Edit today opens the calendar modal directly for today
- Jump to date takes you to the correct month and opens the modal for that date

Optional: Daily Tarot -> Publish queue shows the next 30 days and lets you one-click publish days that are ready.

Quality checks:

- Daily Tarot -> Content includes an Upcoming readiness report for the next 30 days
- From that report you can jump straight into the Calendar modal for a specific date, optionally auto-copying the previous day's deck/pack and filling empty text

Bookings:

- Daily Tarot -> Bookings covers requests, reading types, and payment options (Pro).

## Analytics (unique visitors)

Analytics tracks unique visitors per day using a lightweight browser cookie. It also tracks views alongside uniques for comparison.

- Range selector: last 7/30/90 days or custom range
- Daily trend charts (unique + views) and a combined view
- Engagement rate and returning visitors
- Notable spikes and a weekly heatmap
- Top Cards, Top Decks, Top Pages, and Sources (unique/views toggle)
- Flow overview (source -> page -> card)
- Export CSV for the active range

Note: single-reading renders of `[daily_tarot]` are counted; archive lists are not.

## AI Integration (Pro, Experimental)

AI generation is Pro-gated and managed under Daily Tarot -> AI Integration. The AI area is marked Experimental in the admin.

### Same-site provider (recommended)

You can run the AI provider on the same WordPress site (so it works from any computer, without your PC running anything).

- Set Daily Tarot -> AI Integration -> Provider -> Provider URL to:
  - `https://YOUR_SITE/wp-json/dtarot/v1/generate`
- In `wp-config.php`, define these constants:
  - `DTAROT_AI_PROVIDER_SECRET` (shared secret; required)
  - `DTAROT_AI_API_KEY` (upstream provider API key; required)
  - Optional: `DTAROT_AI_API_BASE` (default `https://api.openai.com/v1`)
  - Optional: `DTAROT_AI_MODEL` (default `gpt-4o-mini`)

The plugin sends the secret in the `X-DTAROT-Provider-Secret` header.

## Backup / Import / Export

Go to Daily Tarot -> Settings.

- JSON backup export/import (decks, meaning packs, calendar)
- ZIP packs:
  - Deck ZIP export/import (includes images)
  - Meaning Pack ZIP export/import
  - Import from URL (for EDD download links)

### Pack versioning

ZIP packs carry `pack_version`. Import blocks downgrades by default; use Allow downgrade if you intentionally install an older version.

## Automation (publish + email)

Go to Daily Tarot -> AI Integration -> Automation.

- Use Reschedule now if the job shows as Not scheduled.

Email options:

- Optional HTML email sending with a plain-text fallback
- Recipient sources: manual list, internal CTA list, and/or Mailchimp audience
- Subject/body templates support placeholders: `{site}`, `{date}`, `{card}`, `{card_id}`, `{deck}`, `{url}`, `{content}`, `{daily_text}`
- Use Email preview to render a sample (does not send)
- Recommendation: install an SMTP plugin (for example WP Mail SMTP) and connect a transactional email provider for reliable delivery.
  
Mailchimp recipients require an API key + Audience ID in Daily Tarot -> Settings -> CTA.

### AI prefill tomorrow (random card)

On the same page (Daily Tarot -> AI Integration -> Automation) you can enable AI prefill tomorrow:

- At 23:59 (site timezone), if tomorrow is still completely empty, the plugin:
  - uses tomorrow's selected deck/card if already set, otherwise picks a random card from one of your default decks (avoids repeating today's card when possible)
  - generates missing Intro and/or Expanded text (via the AI provider if configured, else built-in templates)
  - saves the day as Draft (it does not publish)

### Diagnostics

- Host diagnostics: ZIP/OpenSSL, writable temp/uploads, PHP limits
- Calendar storage: active backend + migration tools
- Automation: enabled state, next scheduled WP-Cron run, and last run result
- Automation also shows WP-Cron status and a not-scheduled reason

## Bookings

Bookings let visitors request or instantly book a tarot reading.

Admin setup:

- Daily Tarot -> Bookings -> Reading Types: create packages (title, description, duration, optional price, icon/emoji).
- Daily Tarot -> Settings -> Bookings: configure availability (days, hours, buffers, blackout dates, timezone).
- Daily Tarot -> Bookings -> Options: configure payment timing and provider links (PayPal / Stripe). Payment options are Pro-only.

Booking modes:

- Request Only: bookings are created as pending and require manual approval.
- Instant Booking: bookings are auto-confirmed.

Payment timing (Pro):

- Pay before booking: users see PayPal/Stripe links and must confirm payment to submit.
- Pay after confirmation: payment link is included in the confirmation email.

Admin actions:

- Approve / Deny / Complete / Cancel
- Edit appointment time, notes, and payment fields
- Resend confirmation email
- Erase personal data (GDPR)

## REST API (published readings)

Read-only REST endpoints (published days only):

- `GET /wp-json/dtarot/v1/readings/YYYY-MM-DD`
- `GET /wp-json/dtarot/v1/readings/latest?limit=1&from=YYYY-MM-DD`

These endpoints apply the same meaning-pack fallback rules as the frontend. REST requests to these endpoints are counted as views in Daily Tarot -> Analytics.

## Frontend

### Shortcodes

#### `[daily_tarot]`

Settings defaults:

- In Daily Tarot -> Settings -> Shortcode you can set default layout, share links, and theme style (minimal/mystic/modern).
- Empty day fallback: when no card is selected for the day, the shortcode can show a non-clickable back card (random or default deck) and a custom message text.

Additional attributes:

- `theme` (`minimal`, `mystic`, `modern`)

Common examples:

```
[daily_tarot]
[daily_tarot layout="hero"]
[daily_tarot mode="archive" days="14" show_date="1"]
[daily_tarot date="2025-12-19"]
```

Supported attributes:

- `date` (YYYY-MM-DD; default today)
- `mode` (`latest`, `archive`, `readable`)
- `days` (archive only; 1..90)
- `show_date` (0/1)
- `layout` (`minimal`, `hero`)
- `show_share` (0/1; hero can default on)
- `readable_url` (optional base URL override)
- `theme` (`minimal`, `mystic`, `modern`)

#### Default deck selection (Tarot / Kipper / Lenormand)

- In Decks (Daily Tarot -> Content -> Decks list), you can click Set Default for a deck.
- Only one default per family is stored at a time (one Tarot, one Lenormand, one Kipper).
- If defaults are configured:
  - `[dtarot_decks]` shows only the default deck(s)
  - `[dtarot_deck]` (no `id`) automatically shows the default deck for the given `system`

#### Default meaning pack selection

- In Meaning Packs (Daily Tarot -> Content -> Meaning Packs list), you can click Set Default for a meaning pack.
- Only one default meaning pack is stored per system.
- Card detail pages (`/cards/{system}/{card_id}/`) use the default meaning pack automatically when no `pack_id` override is provided.

Examples:

- `[dtarot_decks]`
- `[dtarot_deck]`
- `[dtarot_deck system="tarot"]`
- `[dtarot_deck system="kipper"]`
- `[dtarot_deck system="lenormand"]`
- `[dtarot_deck system="kipper_fin_de_siecle"]`
- `[dtarot_decks default_only="0"]` (force show all decks)

Majors/Minors (Tarot only):

- `[dtarot_majors]` (uses default Tarot deck)
- `[dtarot_minors]` (uses default Tarot deck)
- `[dtarot_minors suit="wands"]`

#### `[dtarot_decks]`

Shows a gallery of all decks.

```
[dtarot_decks]
```

#### `[dtarot_deck]`

Shows a grid of all cards for one deck.

```
[dtarot_deck id="123"]
[dtarot_deck id="123" columns="6"]
```

Attributes:

- `id` (required)
- `columns` (2..10)

#### `[dtarot_email_cta]` (Mailchimp or WP)

Renders a simple email signup call-to-action (email input + button). Provider can be Mailchimp (posts to your embed form) or WordPress (stores signups and emails an admin recipient).

You can configure defaults in Daily Tarot -> Settings -> CTA. If configured there, the shortcode works without an `action` attribute (the shortcode attribute still overrides settings).

```
[dtarot_email_cta]
[dtarot_email_cta text="Input your email for daily cards and more." button="Subscribe"]
[dtarot_email_cta action="https://YOURDC.list-manage.com/subscribe/post?u=XXX&id=YYY"]
```

Optional: if Mailchimp gives you a hidden honeypot input name (usually something like `b_xxx_yyy`), you can include it (or set it in Settings -> CTA):

```
[dtarot_email_cta action="https://YOURDC.list-manage.com/subscribe/post?u=XXX&id=YYY" honeypot_name="b_xxx_yyy"]
```

Attributes:

- `action` (optional) - Mailchimp form action URL (overrides Settings -> CTA)
- `text` (optional)
- `placeholder` (optional; default "Your email")
- `button` (optional; default "Subscribe")
- `disclaimer` (optional)
- `target_blank` (0/1; default 1) - submit in a new tab
- `honeypot_name` (optional) - hidden anti-bot field name from Mailchimp embed code (overrides Settings -> CTA)

Note: If provider is set to WordPress, the form posts to `admin-post.php` and stores signups for internal use.

#### `[dtarot_booking]`

Booking form (reading types, time slots, and contact details).

```
[dtarot_booking]
[dtarot_booking style="mystic"]
```

Attributes:

- `style` (`modern`, `mystic`, `minimal`)

#### `[dtarot_booking_button]`

Popup button that opens the booking form.

```
[dtarot_booking_button]
[dtarot_booking_button label="Request a Reading" style="minimal"]
```

Attributes:

- `label` (optional)
- `style` (`modern`, `mystic`, `minimal`)

#### `[dtarot_booking_teaser]`

Teaser card with reading list that opens the booking form.

```
[dtarot_booking_teaser]
[dtarot_booking_teaser title="Daily Tarot Reading" text="Get clarity and guidance." label="Request a Reading" style="modern"]
```

Attributes:

- `title` (optional)
- `text` (optional)
- `label` (optional)
- `style` (`modern`, `mystic`, `minimal`)

#### `[dtarot_majors]`

Shows a grid of Major Arcana cards for a Tarot deck.

```
[dtarot_majors id="123"]
[dtarot_majors id="123" columns="6"]
```

Attributes:

- `id` (or `deck_id`) - required
- `columns` (2..10)

#### `[dtarot_minors]`

Shows a grid of Minor Arcana cards for a Tarot deck.

```
[dtarot_minors id="123"]
[dtarot_minors id="123" columns="6"]
[dtarot_minors id="123" suit="wands"]
[dtarot_minors id="123" suit="cups" columns="6"]
```

Attributes:

- `id` (or `deck_id`) - required
- `columns` (2..10)
- `suit` (optional: `wands`, `cups`, `swords`, `pentacles`; `coins` is accepted as an alias for `pentacles`)

#### `[dtarot_card]`

Shows a single card image/title from a deck.

```
[dtarot_card deck_id="123" card="tarot_major_0"]
[dtarot_card deck_id="123" card="lenormand_01" title="0"]
```

Attributes:

- `deck_id` (required)
- `card` (required card ID)
- `title` (0/1)
- `link` (0/1; default 1) - links to the card detail URL when available

#### `[dtarot_spread]`

Shows a spread (multiple cards) from one deck.

```
[dtarot_spread deck_id="123" cards="tarot_major_0,tarot_major_1,tarot_major_2" columns="3" titles="0"]
[dtarot_spread preset="three_card" spread_pack="default" show_labels="1" show_meanings="1"]
```

Attributes:

- `deck_id` (required)
- `cards` (required comma-separated IDs)
- `columns` (2..10)
- `titles` (0/1)
- `link` (0/1; default 1) - links to the card detail URL when available
- `preset` (optional preset ID)
- `spread_pack` (optional spread meaning pack ID)
- `show_labels` (0/1)
- `show_meanings` (0/1)

Spread layout notes:

- Presets live in `includes/Support/SpreadPresets.php`.
- Slots can include optional layout hints for intentional overlaps.

Example slot config with overlap:

```json
{
  "id": 2,
  "label": "Challenge",
  "x": 0.4,
  "y": 0.45,
  "overlap": true,
  "zIndex": 2
}
```

Rules:

- Slots without `overlap: true` render in the normal grid with `gap: 2rem`.
- Only `overlap: true` slots are positioned absolutely at their `x/y` coordinates.
- Use `zIndex` to control stacking for overlapping cards.
- Overlap slots keep their label + meaning in the grid; only the card image overlaps.
- On mobile (<768px), overlap is disabled and cards fall back to the normal grid flow.

Spread settings:

- Daily Tarot -> Spreads lets you scan for `[dtarot_spread]` shortcodes and map each instance to a preset + meaning pack.
- Spreads -> Options stores defaults and imports spread meaning packs.

#### `[dtarot_month]`

Monthly archive grid (published days only), linking to the readable day pages.

```
[dtarot_month]
[dtarot_month year="2025" month="12"]
[dtarot_month show_card="0"]
[dtarot_month show_excerpt="1"]
```

Attributes:

- `year` (default current year)
- `month` (1..12; default current month)
- `show_card` (0/1; default 1) - shows the card name under the day number
- `show_excerpt` (0/1; default 0) - shows a short intro excerpt (from `content`)

Navigation:

- The shortcode outputs Previous / Today / Next links using `dtarot_year` and `dtarot_month` query args.

Performance:

- Month rendering fetches published entries for the month in a single call (instead of per-day lookups) and uses a small transient/object-cache layer that is invalidated automatically when calendar entries are saved.

SEO readable route:

- `/card-of-the-day/YYYY-MM-DD/`

Share previews:

- Meta description (HTML/OG/Twitter) prefers the short intro text (`content`) when available and falls back to meaning-pack text.

### Card detail URL

Card detail readable route:

- `/cards/{system}/{card_id}/`
  - `system` is one of: `tarot`, `lenormand`, `kipper`
  - `card_id` is a canonical registry ID (e.g. `tarot_major_0`, `lenormand_01`, `kipper_12`)

Notes:

- Rewrite rules are registered on init and flushed on plugin activation. If this URL 404s after an update, re-save permalinks.
- The route renders using the active theme (via `get_header()` / `get_footer()`).
- Images: uses the most recent Deck for that system (if present). If no matching deck exists, the page still renders but without an image.
- Meanings: uses the most recent Meaning Pack for that system (if present). If no pack exists, the page renders with title (and image if available), but no meaning text.
- Optional overrides (query args):
  - `?deck_id=123` forces a specific Deck (must match the route `system`)
  - `?pack_id=456` forces a specific Meaning Pack (must match the route `system`)
  - When overrides are active, the card page shows a small override-active note.

## Gutenberg blocks

Blocks are server-rendered wrappers around the existing shortcodes:

- Daily Tarot - `[daily_tarot]`
- Deck / Cards Gallery - `[dtarot_decks]` or `[dtarot_deck]`
- Single Card Details - `[dtarot_card]`
- Spread - `[dtarot_spread]`
- Card Page - embeds the same renderer as `/cards/{system}/{card_id}/`
- Booking - `[dtarot_booking]` / `[dtarot_booking_button]` / `[dtarot_booking_teaser]`

## Troubleshooting

- Admin buttons do nothing: hard refresh / purge cache; admin assets are cache-busted by file modification time.
- Readable route 404s: re-save permalinks and ensure the day is published.
- Month/archive changes not showing: calendar saves invalidate the plugin's cache automatically, but if you're behind a cache plugin/CDN, purge that cache too.

## Translation

- Default card names are written in English and translated via WordPress (text domain `daily-tarot`).
- You can also override registry names in code via filters: `dtarot_cards_tarot`, `dtarot_cards_lenormand`, `dtarot_cards_kipper`.

## Where things live (dev pointers)

- Plugin bootstrap: [daily-tarot.php](daily-tarot.php)
- Wiring: [includes/Plugin.php](includes/Plugin.php)
- Admin UI: [includes/Admin/Pages](includes/Admin/Pages)
- Pack/backup system: [includes/Admin/Backup.php](includes/Admin/Backup.php)
- Shortcodes: [includes/Frontend/Shortcodes.php](includes/Frontend/Shortcodes.php)
- Feedback (admin-only): Daily Tarot -> Feedback

## Tests (dev)

PHPUnit is installed via Composer.

- Install: `composer install`
- Run: `./vendor/bin/phpunit -c phpunit.xml.dist`
