A clean, modern, fully configurable visual directory template.
Build beautiful, responsive directories with live webpage thumbnails, interactive maps, contact forms, and dark mode — all driven by a single JSON file.
Perfect for churches, clubs, teams, resources, businesses, schools, or any collection of entities.
✨ Live Demo: https://seancrites.github.io/visdir/
- Beautiful card layout with auto-updating webpage thumbnails
- Interactive Leaflet map with location pins
- Dark / Light mode toggle
- Clickable phone, email, social links, and Google Maps
- Secure anti-spam contact form with multiple CAPTCHA provider options
- Everything configurable via one
data.jsonfile
cd /var/www
git clone https://github.com/seancrites/visdir.git
cd visdirThe included deploy.sh script handles everything: it copies files to your web root, applies all custom settings, activates your chosen CAPTCHA provider, and configures contact form and thumbnail paths — all interactively. Settings are saved permanently, so future upgrades via git pull followed by ./deploy.sh will re-apply them automatically.
./deploy.shYou will be prompted for:
| Setting | Description |
|---|---|
| Web root folder | Where index.html should live (e.g. /var/www/html) |
| Scripts folder | Where helper scripts should be deployed |
| Site title | Displayed in the nav bar, header, and SEO <title> tag |
| Base URL | Your site's full URL, including https:// |
| Meta description | Short description for search engine results |
| Open Graph desc. | Description that appears when shared on social media |
| Contact "To" email | Where contact form submissions are sent |
| Contact "From" | The sender address for emails sent by the form |
| Min submit seconds | Anti-spam minimum time before a form can be submitted |
| Enforce referer | Anti-spam check that validates the form's origin |
| CAPTCHA provider | Turnstile, reCAPTCHA v3, hCaptcha, or None |
After reviewing the deployment summary, confirm with y to proceed. The script will:
- Save all settings to
deploy-settings.json(auto-generated, gitignored) — these persist across futuregit pull+./deploy.shupgrades - Back up any existing files in the web root to
archive/backup-YYYYMMDD-HHMMSS/ - Copy fresh
public_html/files to the web root - Copy the
scripts/directory to your scripts folder - Apply every setting: URL replacements, site title, meta/OG descriptions, contact form addresses, anti-spam thresholds, and CAPTCHA activation
- Update
data.jsonwith your site title (existing entity data is preserved)
Need to change a setting later? Just run
./deploy.shagain — all previously saved values will be pre-filled. Edit what you need, confirm, and the script updates only the changed settings while keeping everything else intact.
All customizations are stored in deploy-settings.json (listed in .gitignore so it never gets committed or overwritten by upstream changes). This means:
- ✅
git fetch/git mergewill never clobber your configuration - ✅ Re-running
./deploy.shafter agit pullre-applies all your settings automatically - ✅ Thumbnails, data.json entity data, and generated content are never touched
- ✅ A full backup is always created before any files are overwritten
- ✅ Script paths and relative references are auto-configured to match your environment
⚠️ Warning: Custom changes to deployed files will be overwritten. Every re-run of./deploy.shcopies fresh files frompublic_html/to your web root, overwriting any custom modifications you may have made directly to deployed files (e.g.,index.html,contact.html,contact.php, CSS, JavaScript). A timestamped backup of your web root is always saved toarchive/backup-YYYYMMDD-HHMMSS/before new files are copied, but the only way to preserve custom code permanently is to add it upstream in the repository'spublic_html/source files. Files not inpublic_html/— such asdata.json, thumbnails, anddeploy-settings.json— are never overwritten.
After deployment, your site is live but still needs real data and (optionally) thumbnails:
- Edit
data.json— populate theentitiesarray with your directory listings (see Configuration below) - Run the thumbnail updater — see Thumbnail Setup
- Set up cron — schedule automatic thumbnail refreshes (see Automate with Cron)
Update public_html/data.json with your own site info and entities.
Note for deploy.sh users: The script automatically updates the
site.namefield indata.json. Entity data you add is never overwritten by re-runningdeploy.sh.
| Field | Type | Description |
|---|---|---|
name |
string | Site title displayed in the nav and header |
slogan |
string | Subtitle shown under the site name |
motto |
string | Short phrase displayed in the footer |
year |
number | Copyright year |
maintainer |
string | Name shown in the footer as "Maintained by [name]" (omit to hide) |
show_contact |
boolean | Set to true to show a "Contact" link in the footer |
contact_label |
string | Label shown before each entity's contact_name in the cards (default: Contact) |
support_url |
string | URL for the support link in the footer |
support_label |
string | Label for the support link (shown only if support_url is also set) |
logo_svg |
string | Inline SVG markup for the nav logo |
Each object in the entities array supports:
| Field | Type | Description |
|---|---|---|
slug |
string | Unique identifier used for the thumbnail filename |
name |
string | Display name of the entity |
city |
string | City name |
address |
string | Full street address |
website |
string | URL of the entity's website (required for thumbnail generation) |
contact_name |
string | Name of the contact/leader |
contact_email |
string | Contact person's email address |
phone |
string | Phone number (clickable) |
email |
string | Email address (clickable) |
school_url |
string | School/institution website URL (clickable) |
school_label |
string | Optional display label for school (default: shows the URL) |
stream_url |
string | Live stream URL |
facebook |
string | Facebook page URL |
youtube |
string | YouTube channel URL |
lat |
number | Latitude for the map pin |
lng |
number | Longitude for the map pin |
take_thumbnail |
boolean | Set to false to skip thumbnail generation for this entity |
disable |
boolean | Set to false to skip rendering and thumbnail generation |
Entities support unlimited number of contacts using the following pattern:
| Field | Type | Description |
|---|---|---|
contactN_name |
string | Required. Name of contact person (N = 1, 2, 3, ...) |
contactN_label |
string | Optional label for this contact (default: Contact) |
contactN_email |
string | Email address for this contact |
contactN_phone |
string | Phone number for this contact |
- Start numbering at
1and increment sequentially (contact1_name,contact2_name, etc) - There is no maximum limit - add as many contacts as needed
- If
contactN_nameis missing or empty, that contact and all higher-numbered contacts will be skipped - Legacy Compatibility: The old style single
contact_namefield is fully supported - Improved Behaviour: You may use both singular
contact_nameand numberedcontactN_namefields at the same time. Legacy singular contact will be shown first, followed by all numbered contacts in order.
Entities support unlimited number of schools using the following pattern:
| Field | Type | Description |
|---|---|---|
schoolN_url |
string | Required. URL for school/institution (N = 1, 2, 3, ...) |
schoolN_label |
string | Optional display label for this school (default: shows the URL) |
- Start numbering at
1and increment sequentially (school1_url,school2_url, etc) - There is no maximum limit - add as many schools as needed
- If
schoolN_urlis missing or empty, that school and all higher-numbered schools will be skipped - Legacy Compatibility: The old style single
schoolfield is fully supported - Improved Behaviour: You may use both singular
schooland numberedschoolN_urlfields at the same time. Legacy singular school will be shown first, followed by all numbered schools in order.
The footer is built dynamically from the site fields above:
- If
maintaineris set:Maintained by [maintainer] - If
show_contactistrue:Contact→ links tocontact.html - If
support_urlandsupport_labelare set:[support_label]→ links tosupport_url - Always ends with:
Built with VisDir→ links tohttps://github.com/seancrites/visdir
Examples:
- Everything on:
Maintained by John Doe • Contact • Support • Built with VisDir - Everything off:
Built with VisDir
If you used deploy.sh, the base URL, meta description, and OG description have already been applied. If setting up manually, replace these placeholders:
<meta property="og:url" content="https://yourdomain.com">
<link rel="canonical" href="https://yourdomain.com">
<meta name="description" content="Your description here">
<meta property="og:description" content="Your social description here">Replace https://yourdomain.com with your actual domain.
If you used
deploy.sh, the URL has already been applied to these files.
If you used deploy.sh, the contact form email addresses and anti-spam settings have already been configured. If setting up manually, edit public_html/contact.php:
$to = "you@example.com"; // ← Your email
$headers = "From: no-reply@yourdomain.com\r\n"; // ← Your domain# Create venv (run once)
python3 -m venv ~/visdir-env
# Install dependencies
source ~/visdir-env/bin/activate
pip install --upgrade pip
pip install playwright pillow
playwright install chromium./scripts/update-thumbnails.shIf you used
deploy.sh, the script path inupdate-thumbnails.pyhas already been adjusted to match your environment.
To keep thumbnails fresh automatically:
crontab -eAdd (adjust paths to match your setup):
# Daily at 3:00 AM
0 3 * * * /var/www/visdir/scripts/update-thumbnails.sh >/dev/null 2>&1The contact form includes 3 layered, invisible anti-bot protections that stop ~98% of drive-by spam automatically — no CAPTCHAs, no external services, zero user impact:
| Protection | Description | Effectiveness |
|---|---|---|
| CSS Honeypot | Invisible off-screen field that only bots will fill in. Humans will never see this field. | ✅ Blocks 70% of bots |
| Time Gate | Minimum submission delay enforced. No human fills out a form in < 3 seconds. Every bot does. | ✅ Blocks 95% of bots |
| Origin Validation | Only accept form submissions originating from your actual contact page. | ✅ Blocks 99% of direct POST bots |
All spam rejections return a successful response. Bots have no idea they were blocked and will not retry or adapt.
You may adjust these values at the top of contact.php:
// Minimum seconds required to submit form. Recommended: 3
// Set to 0 to disable this check entirely
$MINIMUM_SUBMIT_SECONDS = 3;Refer to the comments inside contact.php for full documentation on each protection.
In addition to the invisible protections above, you can optionally enable a visible CAPTCHA provider for extra protection on high-traffic or sensitive sites.
Supported providers:
- Cloudflare Turnstile (recommended – fast, privacy-friendly, low friction)
- reCAPTCHA v3 (Google)
- hCaptcha
Each provider has pre-written blocks in both contact.html and contact.php, wrapped in comment markers:
HTML: <!-- TURNSTILE-BEGIN ... TURNSTILE-END -->
PHP: /* TURNSTILE-BEGIN ... TURNSTILE-END */
By default, these blocks are commented out (disabled). To activate a provider, you remove the opening comment marker and replace the placeholder tokens with your actual API keys:
| File | Placeholder | Replace with |
|---|---|---|
contact.html |
TURNSTILE_SITE_KEY |
Your Turnstile site key |
contact.html |
RECAPTCHA_SITE_KEY |
Your reCAPTCHA v3 site key |
contact.html |
HCAPTCHA_SITE_KEY |
Your hCaptcha site key |
contact.php |
TURNSTILE_SECRET_KEY |
Your Turnstile secret key |
contact.php |
RECAPTCHA_SECRET_KEY |
Your reCAPTCHA v3 secret key |
contact.php |
HCAPTCHA_SECRET_KEY |
Your hCaptcha secret key |
When you run ./deploy.sh, you will be prompted to choose a CAPTCHA provider and enter your API keys. The script automatically:
- Activates the correct block by removing the comment markers from
contact.htmlandcontact.php - Replaces the
SITE_KEYplaceholder with your actual site key - Replaces the
SECRET_KEYplaceholder with your actual secret key
All other provider blocks remain safely commented out. Re-run ./deploy.sh anytime to switch providers or update keys — your previous choices are pre-filled.
-
Open
public_html/contact.html -
Activate your chosen provider's
<head>script — remove the<!--and-->comment markers around the script tag. For example, for Turnstile:<!-- BEFORE (commented out): --> <!-- TURNSTILE-BEGIN <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> TURNSTILE-END --> <!-- AFTER (active): --> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
The three options in
<head>(lines 41–54):<!-- Option 1: Cloudflare Turnstile (Recommended - privacy friendly) --> <!-- TURNSTILE-BEGIN <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> TURNSTILE-END --> <!-- Option 2: Google reCAPTCHA v3 (Invisible) --> <!-- RECAPTCHA-BEGIN <script src="https://www.google.com/recaptcha/api.js?render=RECAPTCHA_SITE_KEY"></script> RECAPTCHA-END --> <!-- Option 3: hCaptcha --> <!-- HCAPTCHA-BEGIN <script src="https://js.hcaptcha.com/1/api.js" async defer></script> HCAPTCHA-END -->
-
Activate the matching widget in the form body (lines 112–129) — same approach, remove the comment markers. For Turnstile:
<!-- BEFORE (commented out): --> <!-- TURNSTILE-BEGIN <div class="flex justify-center"> <div class="cf-turnstile" data-sitekey="TURNSTILE_SITE_KEY" data-theme="auto"></div> </div> TURNSTILE-END --> <!-- AFTER (active): --> <div class="flex justify-center"> <div class="cf-turnstile" data-sitekey="TURNSTILE_SITE_KEY" data-theme="auto"></div> </div>
-
Replace the site key placeholder — change
TURNSTILE_SITE_KEYto your actual Turnstile site key (orRECAPTCHA_SITE_KEY/HCAPTCHA_SITE_KEYfor other providers). -
If using Google reCAPTCHA v3, also activate the form submit handler at the bottom of
contact.html(lines 227–238) — remove the<!-- RECAPTCHA-BEGIN/RECAPTCHA-END -->markers around the extra<script>block, and replaceRECAPTCHA_SITE_KEYwith your actual site key. -
Open
public_html/contact.phpand activate the validation block for your chosen provider (lines 93–151). Remove the/* MARKER-BEGIN/MARKER-END */comment markers and replace theSECRET_KEYplaceholder. For Turnstile:// BEFORE (commented out): /* TURNSTILE-BEGIN $turnstile_token = $_POST['cf-turnstile-response'] ?? ''; if (empty($turnstile_token)) { header('Location: contact.html?status=error'); exit; } $secret = 'TURNSTILE_SECRET_KEY'; // ... full validation block ... TURNSTILE-END */ // AFTER (active): $turnstile_token = $_POST['cf-turnstile-response'] ?? ''; if (empty($turnstile_token)) { header('Location: contact.html?status=error'); exit; } $secret = '1q2w3e4r5t6y7u8i9o0p'; // ← your actual Turnstile secret key // ... full validation block ...
Security notes:
- All CAPTCHA tokens are validated server-side before any email is sent.
- Failed CAPTCHA attempts return a clear, user-friendly error message.
- The original invisible anti-spam protections (honeypot, time gate, origin validation) remain active at all times — CAPTCHA is an optional extra layer.
- You can toggle a CAPTCHA provider on/off without touching any other code — just add or remove the comment markers.
- The
deploy.shscript handles all of this automatically and safely.
A standalone converter is included in scripts/convert-data.py for bulk editing. It requires no extra packages—only Python 3.
# JSON array → CSV
python scripts/convert-data.py --to-csv public_html/data.json entities.csv
# CSV → JSON (re-uses site object from existing JSON)
python scripts/convert-data.py --to-json entities.csv public_html/data.json --site-from public_html/data.json
# CSV → JSON (uses template site object)
python scripts/convert-data.py --to-json entities.csv public_html/data.jsonNote: CSV stores everything as text, so numeric and boolean fields will become strings on conversion back to JSON. Review the output and adjust types if needed.
If you prefer to set things up by hand instead of using deploy.sh:
# Example for Apache
cp -r public_html/* /var/www/html/
# Or create a symlink (adjust paths as needed)
ln -s $(pwd)/public_html /var/www/html/visdirAfter copying, manually perform the steps described in Configuration above.
Because all settings are saved in deploy-settings.json (gitignored), upgrading is straightforward:
cd /var/www/visdir
# Fetch the latest code
git fetch origin
git merge origin/main
# Re-run deploy — all previous settings are pre-filled
./deploy.shThe script will:
- Detect your existing settings from
deploy-settings.json - Pre-fill every prompt with your current values — just press Enter to accept or type new values
- Create a backup of the current web root before copying new files
- Re-apply every customization — URL, site title, meta tags, contact form config, CAPTCHA keys
- Preserve your data.json entity data and thumbnails
This project is licensed under the GNU General Public License v3.0 (GPL-3.0).
See LICENSE for details.