fixed bad links

This commit is contained in:
Frank Harris 2026-06-15 20:04:55 -05:00
parent b585aec260
commit 7e9a45f014
79 changed files with 2395 additions and 0 deletions

View file

@ -0,0 +1,152 @@
# Gameservers.World Website Module
This module is the public Gameservers.World sales and documentation website.
## Purpose
- Keep the public site inside the GSP repository at `Panel/modules/website/`
- Remove location-dependent include logic from the old standalone `Website/` tree
- Keep marketing pages working even when billing configuration is missing
- Centralize URL and filesystem path handling so the site is portable
## Structure
```text
Panel/modules/website/
index.php
serverlist.php
docs.php
login.php
pricing.php
locations.php
support.php
doc_asset.php
includes/
bootstrap.php
footer.php
header.php
navigation.php
paths.php
assets/
css/
js/
images/
pages/
home.php
game_servers.php
documentation.php
pricing.php
locations.php
support.php
config/
config.example.php
```
## URL helpers
The website uses a central bootstrap instead of scattered relative paths.
- `website_url('serverlist.php')`
- `website_asset('css/site.css')`
- `panel_url()`
- `login_url()`
- `billing_url('order.php?service_id=1')`
- `documentation_url('minecraft')`
## Billing and database behavior
The public site does not include `Panel/modules/billing/includes/config.inc.php` directly.
Instead it:
1. Parses `Panel/includes/config.inc.php` when present
2. Parses `Panel/modules/billing/includes/config.inc.php` when present
3. Uses the discovered database values only for pages that need catalog data
4. Falls back cleanly when no usable billing or panel DB config is available
Effects:
- `index.php`, `docs.php`, `locations.php`, `pricing.php`, and `support.php` still load
- `serverlist.php` shows a clean fallback message instead of a fatal include error
- shared navigation never crashes because billing config is missing
## Documentation source
Customer documentation is read from the existing billing docs directory:
- `Panel/modules/billing/docs/`
This keeps the website portable without duplicating the documentation tree.
## Deployment
Preferred Apache approach:
1. Point the Gameservers.World vhost `DocumentRoot` to `Panel/modules/website`
2. Expose billing separately under `/billing/` with an Apache `Alias`
3. Set `panel_url`, `login_url`, and `billing_base_url` in `config/config.php` or `config/config.local.php`
Example:
```apacheconf
<VirtualHost *:80>
ServerName gameservers.world
ServerAlias www.gameservers.world
DocumentRoot /var/www/html/GSP/Panel/modules/website
<Directory /var/www/html/GSP/Panel/modules/website>
AllowOverride All
Require all granted
DirectoryIndex index.php
</Directory>
Alias /billing /var/www/html/GSP/Panel/modules/billing
<Directory /var/www/html/GSP/Panel/modules/billing>
AllowOverride All
Require all granted
DirectoryIndex index.php
</Directory>
</VirtualHost>
```
Alternate front-controller approach:
- Keep a tiny public `index.php` outside the repo tree that requires `Panel/modules/website/index.php`
- Do not duplicate the full website into a second location
## Configuration
1. Copy `config/config.example.php` to `config/config.php` or `config/config.local.php`
2. Set:
- `public_base_url`
- `billing_base_url`
- `panel_url`
- `login_url`
- support links
3. Keep any environment-specific config out of Git
## Manual link checks
Verify:
1. `index.php`
2. `serverlist.php`
3. `docs.php`
4. `pricing.php`
5. `locations.php`
6. `support.php`
7. `login.php`
8. header navigation
9. footer links
10. mobile navigation
11. asset URLs
12. `billing_url()` destinations
## Portability notes
- No `/var/www/html/...` filesystem assumptions
- No `gameservers.world/panel` assumptions
- No repeated `../../../` path climbing
- Internal page links and assets route through helpers
- Database config is optional for non-catalog pages

View file

@ -0,0 +1,743 @@
:root {
color-scheme: dark;
--bg: #06111f;
--bg-alt: #0b1729;
--panel: rgba(9, 19, 35, 0.9);
--panel-strong: #0f1d33;
--line: rgba(143, 174, 214, 0.18);
--line-strong: rgba(143, 174, 214, 0.28);
--text: #e8f0ff;
--muted: #93a8cb;
--accent: #54a6ff;
--accent-strong: #7bc0ff;
--accent-soft: rgba(84, 166, 255, 0.12);
--success: #89d77b;
--danger: #ff8c7a;
--shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
--radius: 8px;
--content-width: 1180px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
font-family: Inter, "Segoe UI", Arial, sans-serif;
background:
linear-gradient(180deg, rgba(4, 10, 18, 0.72) 0%, rgba(4, 10, 18, 0.92) 55%, rgba(4, 10, 18, 1) 100%),
url("../images/dark.jpg") center/cover fixed no-repeat,
var(--bg);
color: var(--text);
line-height: 1.5;
}
a {
color: inherit;
text-decoration: none;
}
img {
max-width: 100%;
display: block;
}
button,
input,
select,
textarea {
font: inherit;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.container {
width: min(calc(100% - 32px), var(--content-width));
margin: 0 auto;
}
.site-main {
padding-bottom: 72px;
}
.site-header {
position: sticky;
top: 0;
z-index: 20;
backdrop-filter: blur(16px);
background: rgba(5, 12, 23, 0.88);
border-bottom: 1px solid var(--line);
}
.header-shell {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 24px;
min-height: 76px;
}
.brand,
.footer-brand {
display: inline-flex;
align-items: center;
gap: 14px;
}
.brand-logo,
.footer-logo {
width: 42px;
height: 42px;
}
.brand-copy {
display: grid;
gap: 4px;
}
.brand-name {
font-size: 1rem;
font-weight: 700;
}
.brand-tagline {
color: var(--muted);
font-size: 0.8rem;
}
.primary-nav {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
}
.nav-link {
padding: 10px 12px;
border-radius: 6px;
color: var(--muted);
}
.nav-link:hover,
.nav-link.is-active {
color: var(--text);
background: var(--accent-soft);
}
.header-actions {
display: flex;
gap: 12px;
align-items: center;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 0 18px;
border-radius: 6px;
border: 1px solid transparent;
font-weight: 600;
transition: 0.18s ease;
}
.button:hover {
transform: translateY(-1px);
}
.button-primary {
background: linear-gradient(180deg, #58abff 0%, #3589e8 100%);
color: #051120;
}
.button-secondary {
background: transparent;
color: var(--text);
border-color: var(--line-strong);
}
.button-ghost {
background: rgba(255, 255, 255, 0.03);
border-color: var(--line);
color: var(--text);
}
.nav-toggle {
display: none;
width: 44px;
height: 44px;
padding: 0;
border: 1px solid var(--line);
border-radius: 6px;
background: transparent;
color: var(--text);
}
.nav-toggle span {
display: block;
width: 18px;
height: 2px;
background: currentColor;
margin: 4px auto;
}
.hero {
padding: 72px 0 40px;
}
.hero-layout {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
gap: 32px;
align-items: center;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
margin-bottom: 18px;
border-radius: 999px;
background: rgba(84, 166, 255, 0.12);
border: 1px solid rgba(84, 166, 255, 0.22);
color: #b6d8ff;
font-size: 0.88rem;
font-weight: 600;
}
.hero h1 {
margin: 0 0 16px;
font-size: clamp(2.5rem, 4vw, 4.4rem);
line-height: 1.02;
}
.hero p {
margin: 0;
max-width: 62ch;
color: #c3d2ea;
font-size: 1.05rem;
}
.hero-actions,
.stack-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 24px;
}
.hero-points {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-top: 26px;
}
.hero-point,
.info-card,
.stat-card,
.feature-card,
.location-card,
.service-card,
.support-card,
.doc-card,
.empty-state,
.panel-preview,
.flow-step,
.summary-card {
background: linear-gradient(180deg, rgba(13, 24, 43, 0.92) 0%, rgba(10, 18, 32, 0.92) 100%);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.hero-point {
padding: 16px;
}
.hero-point strong,
.section-heading h2,
.service-card h3,
.feature-card h3,
.location-card h3,
.doc-card h3,
.support-card h3,
.flow-step h3,
.summary-card h3,
.panel-preview h3 {
display: block;
margin: 0 0 8px;
}
.hero-visual {
position: relative;
overflow: hidden;
}
.hero-visual img {
width: 100%;
min-height: 420px;
object-fit: cover;
border-radius: var(--radius);
border: 1px solid var(--line);
box-shadow: var(--shadow);
}
.hero-visual-card {
position: absolute;
left: 24px;
right: 24px;
bottom: 24px;
padding: 18px;
background: rgba(5, 12, 23, 0.82);
border: 1px solid var(--line);
border-radius: 6px;
backdrop-filter: blur(18px);
}
.hero-visual-card strong {
font-size: 1rem;
}
.hero-visual-card p {
color: var(--muted);
margin-top: 8px;
}
.section {
padding: 28px 0;
}
.section-heading {
display: grid;
gap: 10px;
margin-bottom: 22px;
}
.section-heading h2 {
margin: 0;
font-size: clamp(1.6rem, 2vw, 2.2rem);
}
.section-heading p {
margin: 0;
color: var(--muted);
max-width: 72ch;
}
.stats-grid,
.feature-grid,
.location-grid,
.service-grid,
.support-grid,
.doc-grid,
.flow-grid,
.summary-grid {
display: grid;
gap: 18px;
}
.stats-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.feature-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.location-grid,
.service-grid,
.support-grid,
.doc-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.flow-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.stat-card,
.feature-card,
.location-card,
.service-card,
.support-card,
.doc-card,
.flow-step,
.summary-card,
.panel-preview,
.empty-state {
padding: 22px;
}
.stat-card strong {
font-size: 1.65rem;
}
.stat-card span,
.feature-card p,
.location-card p,
.service-card p,
.doc-card p,
.support-card p,
.panel-preview p,
.flow-step p,
.summary-card p,
.muted {
color: var(--muted);
}
.section-divider {
height: 1px;
margin: 16px 0 8px;
background: linear-gradient(90deg, rgba(84, 166, 255, 0) 0%, rgba(84, 166, 255, 0.35) 50%, rgba(84, 166, 255, 0) 100%);
}
.location-card small,
.service-meta,
.doc-meta {
color: #b8cae5;
}
.service-card img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 6px;
border: 1px solid var(--line);
margin-bottom: 16px;
background: #08111f;
}
.service-card header,
.doc-card header {
margin-bottom: 10px;
}
.service-price {
font-size: 1.2rem;
font-weight: 700;
color: var(--accent-strong);
}
.card-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 16px;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 999px;
background: rgba(137, 215, 123, 0.12);
border: 1px solid rgba(137, 215, 123, 0.22);
color: #b6efad;
font-size: 0.85rem;
font-weight: 600;
}
.alert {
padding: 16px 18px;
border-radius: 6px;
border: 1px solid rgba(255, 140, 122, 0.22);
background: rgba(255, 140, 122, 0.08);
color: #ffd1c8;
}
.alert.info {
border-color: rgba(84, 166, 255, 0.2);
background: rgba(84, 166, 255, 0.08);
color: #cbe1ff;
}
.empty-state {
text-align: center;
}
.page-heading {
padding: 54px 0 18px;
}
.page-heading h1 {
margin: 0 0 12px;
font-size: clamp(2rem, 3vw, 3rem);
}
.page-heading p {
margin: 0;
max-width: 74ch;
color: var(--muted);
}
.panel-preview {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(260px, 0.85fr);
gap: 24px;
align-items: center;
}
.panel-preview-shell {
border: 1px solid var(--line);
border-radius: 6px;
overflow: hidden;
}
.panel-preview-shell img {
width: 100%;
aspect-ratio: 16 / 10;
object-fit: cover;
}
.panel-preview-stats {
display: grid;
gap: 12px;
}
.flow-step-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 999px;
background: rgba(84, 166, 255, 0.16);
color: var(--accent-strong);
font-weight: 700;
margin-bottom: 12px;
}
.doc-list-group {
display: grid;
gap: 18px;
}
.doc-list-group h2 {
margin: 0;
font-size: 1.25rem;
}
.doc-view {
padding: 28px;
}
.doc-view h1,
.doc-view h2,
.doc-view h3,
.doc-view h4 {
color: var(--text);
}
.doc-view a {
color: var(--accent-strong);
}
.doc-view pre,
.doc-view code {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
}
.doc-view pre {
overflow-x: auto;
padding: 16px;
border-radius: 6px;
background: rgba(0, 0, 0, 0.28);
border: 1px solid var(--line);
}
.doc-view table {
width: 100%;
border-collapse: collapse;
}
.doc-view th,
.doc-view td {
padding: 10px 12px;
border: 1px solid var(--line);
text-align: left;
}
.cta-band {
padding: 28px 0 0;
}
.cta-panel {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 16px;
align-items: center;
padding: 24px 26px;
background: linear-gradient(135deg, rgba(29, 72, 138, 0.55) 0%, rgba(12, 25, 45, 0.95) 100%);
border: 1px solid rgba(113, 176, 255, 0.28);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.site-footer {
border-top: 1px solid var(--line);
background: rgba(4, 10, 18, 0.78);
padding: 40px 0;
}
.footer-grid {
display: grid;
grid-template-columns: 1.2fr 1fr 1fr 1fr;
gap: 24px;
}
.footer-grid h2 {
font-size: 0.95rem;
margin: 0 0 14px;
}
.footer-copy {
color: var(--muted);
max-width: 32ch;
}
.footer-links {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 10px;
color: var(--muted);
}
.footer-links a:hover {
color: var(--text);
}
@media (max-width: 1080px) {
.hero-layout,
.panel-preview,
.footer-grid {
grid-template-columns: 1fr;
}
.stats-grid,
.feature-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.location-grid,
.service-grid,
.support-grid,
.doc-grid,
.flow-grid,
.summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 820px) {
.header-shell {
grid-template-columns: auto auto;
gap: 16px;
}
.nav-toggle {
display: inline-block;
justify-self: end;
}
.primary-nav,
.header-actions {
display: none;
}
.primary-nav.is-open,
.header-actions.is-open {
display: flex;
}
.header-shell {
padding: 14px 0;
}
.site-header .container {
display: grid;
}
.primary-nav.is-open {
grid-column: 1 / -1;
flex-direction: column;
align-items: stretch;
padding-top: 12px;
}
.header-actions {
grid-column: 1 / -1;
display: none;
flex-wrap: wrap;
margin-top: 6px;
}
.header-actions.is-open {
display: flex;
}
.header-actions .button {
flex: 1 1 180px;
}
.hero,
.page-heading {
padding-top: 42px;
}
}
@media (max-width: 640px) {
.container {
width: min(calc(100% - 24px), var(--content-width));
}
.hero-points,
.stats-grid,
.feature-grid,
.location-grid,
.service-grid,
.support-grid,
.doc-grid,
.flow-grid,
.summary-grid {
grid-template-columns: 1fr;
}
.hero h1 {
font-size: 2.2rem;
}
.cta-panel,
.site-footer {
padding-left: 0;
padding-right: 0;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

View file

@ -0,0 +1,18 @@
document.addEventListener('DOMContentLoaded', () => {
const toggle = document.querySelector('[data-nav-toggle]');
const menu = document.querySelector('[data-nav-menu]');
const actions = document.querySelector('[data-header-actions]');
if (!toggle || !menu) {
return;
}
toggle.addEventListener('click', () => {
const expanded = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', expanded ? 'false' : 'true');
menu.classList.toggle('is-open', !expanded);
if (actions) {
actions.classList.toggle('is-open', !expanded);
}
});
});

View file

@ -0,0 +1,3 @@
config.php
config.local.php

View file

@ -0,0 +1,39 @@
<?php
return [
'site_name' => 'Gameservers.World',
'site_tagline' => 'Virtual private game servers with dedicated resources and full configuration access.',
'meta_description' => 'Virtual private game servers with dedicated resources, predictable performance, full configuration access, mod support, and real human support.',
// Leave null to derive the base path from the current request.
// Example: '/sales'
'base_path' => null,
// Optional absolute public base URL without trailing slash.
// Example: 'https://gameservers.world'
'public_base_url' => 'https://gameservers.world',
// Public billing/catalog/order surface. Recommended Apache alias: /billing
'billing_base_url' => 'https://gameservers.world/billing',
// Active panel URL. Do not point the public site at /panel/ unless that route is real.
'panel_url' => 'https://panel.iaregamer.com/',
'login_url' => 'https://panel.iaregamer.com/',
// Optional support links.
'discord_url' => 'https://discord.gg/replace-me',
'support_url' => 'https://gameservers.world/support',
'support_email' => 'support@gameservers.world',
// Public fallback copy for catalog outages or missing billing config.
'admin_notice' => 'Server catalog is currently unavailable. Please contact support.',
'locations' => [
['name' => 'Los Angeles, USA', 'region' => 'West Coast coverage', 'host' => 'la-game-1.iaregamer.com'],
['name' => 'Kansas City, USA', 'region' => 'Central US coverage', 'host' => 'kc-game-2.iaregamer.com'],
['name' => 'Dallas, USA', 'region' => 'Southern US coverage', 'host' => 'dal-game-1.iaregamer.com'],
['name' => 'New York City, USA', 'region' => 'East Coast coverage', 'host' => 'nyc-game-1.iaregamer.com'],
['name' => 'Dublin, Ireland', 'region' => 'EU coverage', 'host' => 'dub-game-1.iaregamer.com'],
],
];

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
$doc = trim((string)($_GET['doc'] ?? ''));
$file = trim((string)($_GET['file'] ?? ''));
if (!website_is_valid_doc_slug($doc) || !preg_match('/^[a-z0-9._-]+\.(png|jpe?g|webp)$/i', $file)) {
http_response_code(404);
exit;
}
$assetPath = website_doc_path($doc, $file);
if ($assetPath === null || !is_readable($assetPath)) {
http_response_code(404);
exit;
}
$mimeType = match (strtolower(pathinfo($assetPath, PATHINFO_EXTENSION))) {
'png' => 'image/png',
'jpg', 'jpeg' => 'image/jpeg',
'webp' => 'image/webp',
default => 'application/octet-stream',
};
header('Content-Type: ' . $mimeType);
header('Content-Length: ' . (string)filesize($assetPath));
readfile($assetPath);

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
$docSlug = trim((string)($_GET['doc'] ?? ''));
$docIndex = website_fetch_doc_index();
$docContent = null;
$docEntry = null;
if ($docSlug !== '' && website_is_valid_doc_slug($docSlug)) {
foreach ($docIndex as $entry) {
if ($entry['slug'] === $docSlug) {
$docEntry = $entry;
break;
}
}
if ($docEntry !== null) {
$docPath = website_doc_path($docSlug, 'index.php');
if ($docPath !== null) {
ob_start();
include $docPath;
$docContent = (string)ob_get_clean();
}
}
}
website_render(
'documentation.php',
[
'activePage' => 'docs',
'pageTitle' => $docEntry !== null ? ($docEntry['name'] . ' - Documentation - Gameservers.World') : 'Documentation - Gameservers.World',
'metaDescription' => $docEntry !== null ? $docEntry['description'] : 'Browse server setup, panel usage, and troubleshooting guides for Gameservers.World customers.',
'canonicalPath' => $docEntry !== null ? ('docs.php?doc=' . rawurlencode($docEntry['slug'])) : 'docs.php',
'docIndex' => $docIndex,
'docEntry' => $docEntry,
'docContent' => $docContent,
]
);

View file

@ -0,0 +1,506 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/paths.php';
if (defined('GSP_WEBSITE_BOOTSTRAPPED')) {
return;
}
define('GSP_WEBSITE_BOOTSTRAPPED', true);
error_reporting(E_ALL);
ini_set('display_errors', '0');
$websiteConfig = [];
$websiteConfigFiles = [
WEBSITE_CONFIG_DIR . '/config.php',
WEBSITE_CONFIG_DIR . '/config.local.php',
];
foreach ($websiteConfigFiles as $configFile) {
if (!is_readable($configFile)) {
continue;
}
$loaded = require $configFile;
if (is_array($loaded)) {
$websiteConfig = array_replace_recursive($websiteConfig, $loaded);
}
}
$websiteDefaults = [
'site_name' => 'Gameservers.World',
'site_tagline' => 'Virtual private game servers with dedicated resources and full configuration access.',
'meta_description' => 'Virtual private game servers with dedicated resources, predictable performance, full configuration access, mod support, and real human support.',
'base_path' => null,
'public_base_url' => null,
'billing_base_url' => '/billing',
'panel_url' => 'https://panel.iaregamer.com/',
'login_url' => 'https://panel.iaregamer.com/',
'discord_url' => null,
'support_url' => null,
'support_email' => null,
'admin_notice' => 'Server catalog is currently unavailable. Please contact support.',
'locations' => [
['name' => 'Los Angeles, USA', 'region' => 'West Coast coverage', 'host' => 'la-game-1.iaregamer.com'],
['name' => 'Kansas City, USA', 'region' => 'Central US coverage', 'host' => 'kc-game-2.iaregamer.com'],
['name' => 'Dallas, USA', 'region' => 'Southern US coverage', 'host' => 'dal-game-1.iaregamer.com'],
['name' => 'New York City, USA', 'region' => 'East Coast coverage', 'host' => 'nyc-game-1.iaregamer.com'],
['name' => 'Dublin, Ireland', 'region' => 'EU coverage', 'host' => 'dub-game-1.iaregamer.com'],
],
];
$websiteConfig = array_replace_recursive($websiteDefaults, $websiteConfig);
function website_config(?string $key = null, mixed $default = null): mixed
{
global $websiteConfig;
if ($key === null) {
return $websiteConfig;
}
return $websiteConfig[$key] ?? $default;
}
function website_log(string $message): void
{
error_log('[website] ' . $message);
}
function website_escape(mixed $value): string
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
function website_normalize_base_path(?string $path): string
{
$path = trim((string)$path);
if ($path === '' || $path === '/') {
return '';
}
return '/' . trim($path, '/');
}
function website_request_scheme(): string
{
$https = $_SERVER['HTTPS'] ?? '';
if ($https !== '' && strtolower((string)$https) !== 'off') {
return 'https';
}
$forwarded = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '';
if ($forwarded !== '') {
return strtolower((string)$forwarded) === 'https' ? 'https' : 'http';
}
return 'http';
}
function website_base_path(): string
{
static $basePath = null;
if ($basePath !== null) {
return $basePath;
}
$configured = website_config('base_path');
if (is_string($configured) && $configured !== '') {
$basePath = website_normalize_base_path($configured);
return $basePath;
}
$scriptName = (string)($_SERVER['SCRIPT_NAME'] ?? '');
if ($scriptName === '') {
$basePath = '';
return $basePath;
}
$dir = str_replace('\\', '/', dirname($scriptName));
$basePath = ($dir === '/' || $dir === '.' || $dir === '') ? '' : website_normalize_base_path($dir);
return $basePath;
}
function website_public_base_url(): string
{
static $baseUrl = null;
if ($baseUrl !== null) {
return $baseUrl;
}
$configured = trim((string)website_config('public_base_url', ''));
if ($configured !== '') {
$baseUrl = rtrim($configured, '/');
return $baseUrl;
}
$host = trim((string)($_SERVER['HTTP_HOST'] ?? ''));
if ($host === '') {
$baseUrl = '';
return $baseUrl;
}
$baseUrl = website_request_scheme() . '://' . $host . website_base_path();
return $baseUrl;
}
function website_url(string $path = ''): string
{
$basePath = rtrim(website_base_path(), '/');
$path = ltrim($path, '/');
if ($path === '') {
return $basePath === '' ? '/' : $basePath . '/';
}
return ($basePath === '' ? '' : $basePath) . '/' . $path;
}
function website_asset(string $path): string
{
return website_url('assets/' . ltrim($path, '/'));
}
function website_join_external_url(string $base, string $path = ''): string
{
$base = trim($base);
if ($base === '') {
return website_url($path);
}
$base = rtrim($base, '/');
$path = ltrim($path, '/');
if ($path === '') {
return $base . '/';
}
return $base . '/' . $path;
}
function panel_url(string $path = ''): string
{
return website_join_external_url((string)website_config('panel_url', ''), $path);
}
function login_url(string $path = ''): string
{
return website_join_external_url((string)website_config('login_url', website_config('panel_url', '')), $path);
}
function billing_url(string $path = ''): string
{
return website_join_external_url((string)website_config('billing_base_url', ''), $path);
}
function documentation_url(?string $docSlug = null): string
{
if ($docSlug === null || $docSlug === '') {
return website_url('docs.php');
}
return website_url('docs.php?doc=' . rawurlencode($docSlug));
}
function website_canonical_url(string $path = ''): string
{
$base = website_public_base_url();
if ($base === '') {
return website_url($path);
}
$path = ltrim($path, '/');
if ($path === '') {
return $base . '/';
}
return rtrim($base, '/') . '/' . $path;
}
function website_read_php_assignments(string $filePath, array $variableNames): array
{
if (!is_readable($filePath)) {
return [];
}
$content = @file_get_contents($filePath);
if ($content === false) {
return [];
}
$result = [];
foreach ($variableNames as $variableName) {
$patternDouble = '/^\s*\$' . preg_quote($variableName, '/') . '\s*=\s*"([^"]*)"/m';
$patternSingle = '/^\s*\$' . preg_quote($variableName, '/') . "\s*=\s*'([^']*)'/m";
if (preg_match($patternDouble, $content, $match) === 1 || preg_match($patternSingle, $content, $match) === 1) {
$result[$variableName] = $match[1];
}
}
return $result;
}
function website_database_settings(): ?array
{
static $settings = null;
static $resolved = false;
if ($resolved) {
return $settings;
}
$resolved = true;
$keys = ['db_host', 'db_port', 'db_user', 'db_pass', 'db_name', 'table_prefix', 'db_type'];
$merged = [];
$panelConfig = WEBSITE_PANEL_INCLUDE_DIR . '/config.inc.php';
if (is_readable($panelConfig)) {
$merged = array_replace($merged, website_read_php_assignments($panelConfig, $keys));
}
$billingConfig = WEBSITE_BILLING_ROOT . '/includes/config.inc.php';
if (is_readable($billingConfig)) {
$merged = array_replace($merged, website_read_php_assignments($billingConfig, $keys));
}
foreach (['db_host', 'db_user', 'db_name', 'table_prefix'] as $requiredKey) {
if (empty($merged[$requiredKey])) {
$settings = null;
return $settings;
}
}
$settings = $merged;
return $settings;
}
function website_billing_config_present(): bool
{
return is_readable(WEBSITE_BILLING_ROOT . '/includes/config.inc.php');
}
function website_db(): ?mysqli
{
static $connection = false;
if ($connection instanceof mysqli) {
return $connection;
}
if ($connection === null) {
return null;
}
$settings = website_database_settings();
if ($settings === null) {
$connection = null;
return null;
}
$port = isset($settings['db_port']) && $settings['db_port'] !== '' ? (int)$settings['db_port'] : null;
$mysqli = @mysqli_connect(
(string)$settings['db_host'],
(string)($settings['db_user'] ?? ''),
(string)($settings['db_pass'] ?? ''),
(string)$settings['db_name'],
$port
);
if (!$mysqli instanceof mysqli) {
website_log('Database connection failed for public website.');
$connection = null;
return null;
}
@mysqli_set_charset($mysqli, 'utf8mb4');
$connection = $mysqli;
return $connection;
}
function website_table_prefix(): string
{
$settings = website_database_settings();
return (string)($settings['table_prefix'] ?? '');
}
function website_billing_available(): bool
{
return website_db() instanceof mysqli;
}
function website_billing_docs_root(): ?string
{
if (is_dir(WEBSITE_BILLING_DOCS_DIR)) {
return WEBSITE_BILLING_DOCS_DIR;
}
$legacyDocs = WEBSITE_LEGACY_SITE_ROOT . '/docs';
if (is_dir($legacyDocs)) {
return $legacyDocs;
}
return null;
}
function website_is_valid_doc_slug(string $slug): bool
{
return (bool)preg_match('/^[a-z0-9][a-z0-9_-]*$/i', $slug);
}
function website_doc_path(string $slug, string $fileName = 'index.php'): ?string
{
if (!website_is_valid_doc_slug($slug)) {
return null;
}
$docsRoot = website_billing_docs_root();
if ($docsRoot === null) {
return null;
}
$candidate = realpath($docsRoot . '/' . $slug . '/' . $fileName);
if ($candidate === false || strpos($candidate, realpath($docsRoot) ?: $docsRoot) !== 0) {
return null;
}
return $candidate;
}
function website_doc_icon_url(string $slug): ?string
{
foreach (['icon.png', 'icon.jpg', 'icon.jpeg', 'icon.webp'] as $fileName) {
if (website_doc_path($slug, $fileName) !== null) {
return website_url('doc_asset.php?doc=' . rawurlencode($slug) . '&file=' . rawurlencode($fileName));
}
}
return null;
}
function website_service_image_url(string $imageValue): string
{
$imageValue = trim($imageValue);
if ($imageValue === '') {
return website_asset('images/banner.png');
}
if (preg_match('#^https?://#i', $imageValue) === 1) {
return $imageValue;
}
$fileName = basename($imageValue);
if ($fileName === '') {
return website_asset('images/banner.png');
}
return website_asset('images/games/' . $fileName);
}
function website_fetch_services(int $limit = 0): array
{
$db = website_db();
if (!$db instanceof mysqli) {
return [];
}
$prefix = website_table_prefix();
if ($prefix === '') {
return [];
}
$sql = "SELECT bs.service_id,
bs.service_name,
bs.description,
bs.img_url,
bs.price_monthly,
bs.remote_server_id,
ch.game_name AS cfg_game_name,
ch.game_key AS cfg_game_key,
ch.home_cfg_file AS cfg_file
FROM `{$prefix}billing_services` bs
LEFT JOIN `{$prefix}config_homes` ch ON ch.home_cfg_id = bs.home_cfg_id
WHERE bs.enabled = 1
AND bs.remote_server_id <> ''
AND bs.remote_server_id IS NOT NULL
ORDER BY bs.service_name ASC";
if ($limit > 0) {
$sql .= ' LIMIT ' . max(1, $limit);
}
$result = @$db->query($sql);
if (!$result instanceof mysqli_result) {
website_log('Failed to query billing services for website catalog.');
return [];
}
$rows = [];
while ($row = $result->fetch_assoc()) {
$rows[] = $row;
}
$result->free();
return $rows;
}
function website_fetch_doc_index(): array
{
$docsRoot = website_billing_docs_root();
if ($docsRoot === null || !is_dir($docsRoot)) {
return [];
}
$entries = [];
foreach (array_diff(scandir($docsRoot) ?: [], ['.', '..']) as $folder) {
$docFolder = $docsRoot . '/' . $folder;
if (!is_dir($docFolder)) {
continue;
}
$indexPath = website_doc_path($folder, 'index.php');
$metadataPath = website_doc_path($folder, 'metadata.json');
if ($indexPath === null || $metadataPath === null) {
continue;
}
$metadataContent = @file_get_contents($metadataPath);
$metadataContent = $metadataContent === false ? '' : preg_replace('/^\xEF\xBB\xBF/', '', $metadataContent);
$metadata = json_decode((string)$metadataContent, true);
if (!is_array($metadata)) {
$metadata = [];
}
$entries[] = [
'slug' => $folder,
'name' => (string)($metadata['name'] ?? ucwords(str_replace(['-', '_'], ' ', $folder))),
'description' => (string)($metadata['description'] ?? ''),
'category' => (string)($metadata['category'] ?? 'other'),
'order' => (int)($metadata['order'] ?? 999),
'complete' => (bool)($metadata['complete'] ?? true),
'icon_url' => website_doc_icon_url($folder),
];
}
usort(
$entries,
static function (array $left, array $right): int {
if ($left['category'] !== $right['category']) {
return strcmp($left['category'], $right['category']);
}
if ($left['order'] !== $right['order']) {
return $left['order'] <=> $right['order'];
}
return strcasecmp($left['name'], $right['name']);
}
);
return $entries;
}
function website_render(string $pageTemplate, array $context = []): void
{
extract($context, EXTR_SKIP);
require WEBSITE_INCLUDE_DIR . '/header.php';
require WEBSITE_ROOT_DIR . '/pages/' . $pageTemplate;
require WEBSITE_INCLUDE_DIR . '/footer.php';
}

View file

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
$discordUrl = trim((string)website_config('discord_url', ''));
$supportUrl = trim((string)website_config('support_url', ''));
$supportEmail = trim((string)website_config('support_email', ''));
?>
</main>
<footer class="site-footer">
<div class="container footer-grid">
<div>
<a class="footer-brand" href="<?= website_escape(website_url('index.php')) ?>">
<img src="<?= website_escape(website_asset('images/logo-sm.png')) ?>" alt="Gameservers.World logo" class="footer-logo">
<span><?= website_escape(website_config('site_name')) ?></span>
</a>
<p class="footer-copy"><?= website_escape(website_config('site_tagline')) ?></p>
</div>
<div>
<h2>Explore</h2>
<ul class="footer-links">
<li><a href="<?= website_escape(website_url('serverlist.php')) ?>">Game Servers</a></li>
<li><a href="<?= website_escape(website_url('docs.php')) ?>">Documentation</a></li>
<li><a href="<?= website_escape(website_url('pricing.php')) ?>">Pricing</a></li>
<li><a href="<?= website_escape(website_url('locations.php')) ?>">Locations</a></li>
<li><a href="<?= website_escape(website_url('support.php')) ?>">Support</a></li>
</ul>
</div>
<div>
<h2>Customer Access</h2>
<ul class="footer-links">
<li><a href="<?= website_escape(login_url()) ?>">Customer Login</a></li>
<li><a href="<?= website_escape(panel_url()) ?>">Control Panel</a></li>
<li><a href="<?= website_escape(website_url('docs.php')) ?>">Server Guides</a></li>
<?php if ($discordUrl !== ''): ?>
<li><a href="<?= website_escape($discordUrl) ?>" target="_blank" rel="noopener noreferrer">Discord</a></li>
<?php endif; ?>
</ul>
</div>
<div>
<h2>Need Help?</h2>
<ul class="footer-links">
<?php if ($supportUrl !== ''): ?>
<li><a href="<?= website_escape($supportUrl) ?>" target="_blank" rel="noopener noreferrer">Support Portal</a></li>
<?php endif; ?>
<?php if ($supportEmail !== ''): ?>
<li><a href="mailto:<?= website_escape($supportEmail) ?>"><?= website_escape($supportEmail) ?></a></li>
<?php endif; ?>
<li><a href="<?= website_escape(website_url('support.php')) ?>">Contact Options</a></li>
</ul>
</div>
</div>
</footer>
<script src="<?= website_escape(website_asset('js/site.js')) ?>" defer></script>
</body>
</html>

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
$pageTitle = $pageTitle ?? website_config('site_name');
$metaDescription = $metaDescription ?? website_config('meta_description');
$canonicalPath = $canonicalPath ?? '';
$socialImage = $socialImage ?? website_asset('images/banner.png');
$bodyClass = $bodyClass ?? '';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= website_escape($pageTitle) ?></title>
<meta name="description" content="<?= website_escape($metaDescription) ?>">
<link rel="canonical" href="<?= website_escape(website_canonical_url($canonicalPath)) ?>">
<meta property="og:title" content="<?= website_escape($pageTitle) ?>">
<meta property="og:description" content="<?= website_escape($metaDescription) ?>">
<meta property="og:type" content="website">
<meta property="og:url" content="<?= website_escape(website_canonical_url($canonicalPath)) ?>">
<meta property="og:image" content="<?= website_escape($socialImage) ?>">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="<?= website_escape($pageTitle) ?>">
<meta name="twitter:description" content="<?= website_escape($metaDescription) ?>">
<meta name="twitter:image" content="<?= website_escape($socialImage) ?>">
<link rel="icon" type="image/png" href="<?= website_escape(website_asset('images/logo-sm.png')) ?>">
<link rel="stylesheet" href="<?= website_escape(website_asset('css/site.css')) ?>">
</head>
<body class="<?= website_escape($bodyClass) ?>">
<?php require WEBSITE_INCLUDE_DIR . '/navigation.php'; ?>
<main class="site-main">

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
$activePage = $activePage ?? '';
$navLinks = [
['key' => 'home', 'label' => 'Home', 'href' => website_url('index.php')],
['key' => 'servers', 'label' => 'Game Servers', 'href' => website_url('serverlist.php')],
['key' => 'docs', 'label' => 'Documentation', 'href' => website_url('docs.php')],
['key' => 'pricing', 'label' => 'Pricing', 'href' => website_url('pricing.php')],
['key' => 'locations', 'label' => 'Locations', 'href' => website_url('locations.php')],
['key' => 'support', 'label' => 'Support', 'href' => website_url('support.php')],
];
?>
<header class="site-header">
<div class="container header-shell">
<a class="brand" href="<?= website_escape(website_url('index.php')) ?>">
<img src="<?= website_escape(website_asset('images/logo-sm.png')) ?>" alt="Gameservers.World logo" class="brand-logo">
<span class="brand-copy">
<span class="brand-name"><?= website_escape(website_config('site_name')) ?></span>
<span class="brand-tagline">Virtual private game servers</span>
</span>
</a>
<button class="nav-toggle" type="button" aria-expanded="false" aria-controls="primary-nav" data-nav-toggle>
<span></span>
<span></span>
<span></span>
<span class="sr-only">Toggle navigation</span>
</button>
<nav class="primary-nav" id="primary-nav" data-nav-menu>
<?php foreach ($navLinks as $link): ?>
<a class="nav-link<?= $activePage === $link['key'] ? ' is-active' : '' ?>" href="<?= website_escape($link['href']) ?>">
<?= website_escape($link['label']) ?>
</a>
<?php endforeach; ?>
</nav>
<div class="header-actions" data-header-actions>
<a class="button button-secondary" href="<?= website_escape(login_url()) ?>">Login</a>
<a class="button button-primary" href="<?= website_escape(panel_url()) ?>">Control Panel</a>
</div>
</div>
</header>

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
if (defined('GSP_WEBSITE_PATHS_LOADED')) {
return;
}
define('GSP_WEBSITE_PATHS_LOADED', true);
define('WEBSITE_ROOT_DIR', realpath(dirname(__DIR__)) ?: dirname(__DIR__));
define('WEBSITE_INCLUDE_DIR', __DIR__);
define('WEBSITE_CONFIG_DIR', WEBSITE_ROOT_DIR . '/config');
define('WEBSITE_ASSET_DIR', WEBSITE_ROOT_DIR . '/assets');
define('WEBSITE_PANEL_ROOT', realpath(dirname(dirname(WEBSITE_ROOT_DIR))) ?: dirname(dirname(WEBSITE_ROOT_DIR)));
define('WEBSITE_PANEL_INCLUDE_DIR', WEBSITE_PANEL_ROOT . '/includes');
define('WEBSITE_MODULES_DIR', WEBSITE_PANEL_ROOT . '/modules');
define('WEBSITE_BILLING_ROOT', WEBSITE_MODULES_DIR . '/billing');
define('WEBSITE_BILLING_DOCS_DIR', WEBSITE_BILLING_ROOT . '/docs');
define('WEBSITE_LEGACY_SITE_ROOT', realpath(dirname(WEBSITE_PANEL_ROOT) . '/Website') ?: dirname(WEBSITE_PANEL_ROOT) . '/Website');

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
$services = website_fetch_services(6);
$locations = website_config('locations', []);
website_render(
'home.php',
[
'activePage' => 'home',
'pageTitle' => 'Gameservers.World - Virtual private game servers',
'metaDescription' => website_config('meta_description'),
'canonicalPath' => 'index.php',
'services' => $services,
'locations' => is_array($locations) ? $locations : [],
]
);

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
website_render(
'locations.php',
[
'activePage' => 'locations',
'pageTitle' => 'Locations - Gameservers.World',
'metaDescription' => 'Current Gameservers.World hosting regions and deployment guidance.',
'canonicalPath' => 'locations.php',
'locations' => website_config('locations', []),
]
);

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
$destination = login_url();
if ($destination !== '') {
header('Location: ' . $destination, true, 302);
exit;
}
website_render(
'support.php',
[
'activePage' => 'support',
'pageTitle' => 'Login - Gameservers.World',
'metaDescription' => 'Customer login routing for Gameservers.World.',
'canonicalPath' => 'login.php',
'loginUnavailable' => true,
]
);

View file

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
$categoryLabels = [
'panel' => 'Panel Documentation',
'game' => 'Game Servers',
'mods' => 'Mods and Plugins',
'troubleshooting' => 'Troubleshooting',
'other' => 'Other',
];
$groupedDocs = [];
foreach ($docIndex as $entry) {
$groupedDocs[$entry['category']][] = $entry;
}
?>
<section class="page-heading">
<div class="container">
<h1><?= $docEntry !== null ? website_escape($docEntry['name']) : 'Documentation' ?></h1>
<p><?= $docEntry !== null ? website_escape((string)($docEntry['description'] ?? '')) : 'Browse customer guides, panel references, and game-specific setup documentation without depending on billing configuration.' ?></p>
</div>
</section>
<section class="section">
<div class="container">
<?php if ($docEntry !== null && $docContent !== null): ?>
<div class="stack-actions" style="margin-bottom: 18px;">
<a class="button button-secondary" href="<?= website_escape(website_url('docs.php')) ?>">Back to documentation</a>
<a class="button button-ghost" href="<?= website_escape(website_url('index.php')) ?>">Home</a>
</div>
<article class="doc-view">
<?= $docContent ?>
</article>
<?php elseif (!empty($docIndex)): ?>
<div class="doc-list-group">
<?php foreach ($groupedDocs as $category => $entries): ?>
<section>
<div class="section-heading">
<h2><?= website_escape($categoryLabels[$category] ?? ucwords($category)) ?></h2>
<p><?= website_escape(($categoryLabels[$category] ?? $category) . ' available through the shared GSP documentation set.') ?></p>
</div>
<div class="doc-grid">
<?php foreach ($entries as $entry): ?>
<article class="doc-card">
<header>
<?php if (!empty($entry['icon_url'])): ?>
<img src="<?= website_escape((string)$entry['icon_url']) ?>" alt="" style="width:56px;height:56px;border-radius:6px;border:1px solid var(--line);margin-bottom:14px;object-fit:cover;">
<?php endif; ?>
<h3><?= website_escape((string)$entry['name']) ?></h3>
<div class="doc-meta"><?= website_escape((string)$entry['category']) ?></div>
</header>
<p><?= website_escape((string)$entry['description']) ?></p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(documentation_url((string)$entry['slug'])) ?>">Open Guide</a>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-state">
<h2>Documentation is unavailable</h2>
<p>No documentation source directory was found. Check that the billing module docs directory is present alongside the website module.</p>
</div>
<?php endif; ?>
</div>
</section>

View file

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
$hasBilling = website_billing_available();
?>
<section class="page-heading">
<div class="container">
<h1>Game Servers</h1>
<p>Available services are pulled from the existing billing catalog when the database is configured. This page no longer reaches into billing includes with hardcoded filesystem paths, so it stays up even when billing config is missing.</p>
</div>
</section>
<section class="section">
<div class="container">
<?php if ($hasBilling && !empty($services)): ?>
<div class="service-grid">
<?php foreach ($services as $service): ?>
<?php
$serviceName = trim((string)($service['cfg_game_name'] ?? $service['service_name'] ?? 'Game Server'));
$description = trim((string)($service['description'] ?? ''));
$price = (float)($service['price_monthly'] ?? 0);
$orderHref = billing_url('order.php?service_id=' . rawurlencode((string)$service['service_id']));
$docSlug = trim((string)($service['cfg_game_key'] ?? ''));
?>
<article class="service-card">
<img src="<?= website_escape(website_service_image_url((string)($service['img_url'] ?? ''))) ?>" alt="<?= website_escape($serviceName) ?>">
<header>
<h3><?= website_escape($serviceName) ?></h3>
<div class="service-meta"><?= website_escape((string)($service['cfg_game_key'] ?? '')) ?></div>
</header>
<p><?= website_escape($description !== '' ? $description : 'Dedicated resources, full configuration access, and GSP panel management.') ?></p>
<div class="section-divider"></div>
<div class="service-price"><?= $price > 0 ? '$' . number_format($price, 2) . ' / month' : 'Contact for pricing' ?></div>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape($orderHref) ?>">Order Now</a>
<?php if ($docSlug !== '' && website_doc_path($docSlug) !== null): ?>
<a class="button button-secondary" href="<?= website_escape(documentation_url($docSlug)) ?>">Documentation</a>
<?php else: ?>
<a class="button button-secondary" href="<?= website_escape(website_url('docs.php')) ?>">Documentation</a>
<?php endif; ?>
</div>
</article>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-state">
<h2>Server catalog unavailable</h2>
<p><?= website_escape((string)website_config('admin_notice')) ?></p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(website_url('support.php')) ?>">Contact Support</a>
<a class="button button-secondary" href="<?= website_escape(website_url('index.php')) ?>">Return Home</a>
</div>
<?php if (!website_billing_config_present()): ?>
<p class="muted">Billing configuration is missing or unreadable. Public pages still load, but service data cannot be queried until billing or panel database credentials are configured.</p>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</section>

View file

@ -0,0 +1,245 @@
<?php
declare(strict_types=1);
$discordUrl = trim((string)website_config('discord_url', ''));
?>
<section class="hero">
<div class="container hero-layout">
<div>
<div class="eyebrow">Dedicated resources. Full control. Real support.</div>
<h1>Virtual private game servers with predictable resources and full configuration access.</h1>
<p><?= website_escape(website_config('site_tagline')) ?> Gameservers.World focuses on stable capacity, older community-favorite titles, mod support, and a control panel that gives customers practical access instead of a stripped-down toy interface.</p>
<div class="hero-actions">
<a class="button button-primary" href="<?= website_escape(website_url('serverlist.php')) ?>">View Game Servers</a>
<a class="button button-secondary" href="<?= website_escape(panel_url()) ?>">Open Control Panel</a>
</div>
<div class="hero-points">
<div class="hero-point">
<strong>Never oversold</strong>
<span class="muted">Dedicated resources stay dedicated. Predictable performance is the product, not an upgrade.</span>
</div>
<div class="hero-point">
<strong>Mod and Workshop support</strong>
<span class="muted">Common modding workflows, Workshop content, and file access stay available where the game allows it.</span>
</div>
<div class="hero-point">
<strong>Control and customization</strong>
<span class="muted">Startup parameters, configs, backups, and updates are managed through the GSP panel without hiding the important controls.</span>
</div>
<div class="hero-point">
<strong>Real people</strong>
<span class="muted">Documentation exists, but you can still talk to someone when an old modpack or legacy title gets awkward.</span>
</div>
</div>
</div>
<div class="hero-visual">
<img src="<?= website_escape(website_asset('images/banner.png')) ?>" alt="Gameservers.World supported games collage">
<div class="hero-visual-card">
<strong>Built for hosted communities that want actual control</strong>
<p>Use the GSP panel for server start/stop, config editing, Workshop or mod workflows, logs, file access, and routine maintenance without depending on fragile manual shell work.</p>
</div>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="stats-grid">
<div class="stat-card">
<strong>Dedicated</strong>
<span>No noisy-neighbor overselling.</span>
</div>
<div class="stat-card">
<strong>Full access</strong>
<span>Configs, startup parameters, files, backups, and updates.</span>
</div>
<div class="stat-card">
<strong>Legacy-friendly</strong>
<span>Older and community-favorite games stay part of the catalog.</span>
</div>
<div class="stat-card">
<strong>Operational help</strong>
<span>Human support when the docs are not enough.</span>
</div>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="section-heading">
<h2>Why Gameservers.World</h2>
<p>The website now makes the actual sales proposition explicit: dedicated capacity, access to the knobs that matter, and hosting for both current and older multiplayer titles without burying customers behind fragile shared-hosting abstractions.</p>
</div>
<div class="feature-grid">
<article class="feature-card">
<h3>Dedicated resources</h3>
<p>Capacity is reserved for the server you pay for. CPU and memory planning are aimed at stable behavior, not squeezing one more tenant onto the node.</p>
</article>
<article class="feature-card">
<h3>Mod support</h3>
<p>Workshop and addon workflows are supported where the underlying game tooling allows it, with file access and update paths that customers can actually use.</p>
</article>
<article class="feature-card">
<h3>Panel control</h3>
<p>Customers manage their service through GSP rather than opening tickets for every config tweak, reboot, backup, or startup parameter change.</p>
</article>
<article class="feature-card">
<h3>Real support</h3>
<p>When an old title, community mod, or provisioning edge case gets weird, support is available through documentation plus direct assistance.</p>
</article>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="section-heading">
<h2>Current locations</h2>
<p>Locations are configured data, not marketing filler. These reflect the currently documented host regions from the existing Gameservers.World site content.</p>
</div>
<div class="location-grid">
<?php foreach ($locations as $location): ?>
<article class="location-card">
<h3><?= website_escape((string)($location['name'] ?? '')) ?></h3>
<p><?= website_escape((string)($location['region'] ?? '')) ?></p>
<small><?= website_escape((string)($location['host'] ?? '')) ?></small>
</article>
<?php endforeach; ?>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="section-heading">
<h2>Popular and supported game servers</h2>
<p>These cards are driven from the existing billing catalog when configuration is available. The site does not invent fake availability if the billing catalog is offline.</p>
</div>
<?php if (!empty($services)): ?>
<div class="service-grid">
<?php foreach ($services as $service): ?>
<?php
$serviceName = trim((string)($service['cfg_game_name'] ?? $service['service_name'] ?? 'Game Server'));
$price = (float)($service['price_monthly'] ?? 0);
$orderUrl = billing_url('order.php?service_id=' . rawurlencode((string)$service['service_id']));
?>
<article class="service-card">
<img src="<?= website_escape(website_service_image_url((string)($service['img_url'] ?? ''))) ?>" alt="<?= website_escape($serviceName) ?>">
<header>
<h3><?= website_escape($serviceName) ?></h3>
<div class="service-price"><?= $price > 0 ? '$' . number_format($price, 2) . ' / month' : 'Contact for pricing' ?></div>
</header>
<p><?= website_escape(trim((string)($service['description'] ?? 'Dedicated hosting with full configuration access.')) ?: 'Dedicated hosting with full configuration access.') ?></p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape($orderUrl) ?>">Order</a>
<a class="button button-ghost" href="<?= website_escape(website_url('serverlist.php')) ?>">Catalog</a>
</div>
</article>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="empty-state">
<h3>Catalog visibility depends on billing data</h3>
<p><?= website_escape((string)website_config('admin_notice')) ?></p>
<div class="card-actions">
<a class="button button-secondary" href="<?= website_escape(website_url('support.php')) ?>">Contact Support</a>
<a class="button button-ghost" href="<?= website_escape(website_url('docs.php')) ?>">Read Documentation</a>
</div>
</div>
<?php endif; ?>
</div>
</section>
<section class="section">
<div class="container">
<div class="section-heading">
<h2>How it works</h2>
<p>The public site and the control plane are now separated cleanly: the website explains the service, the billing catalog handles order-specific data, and GSP runs the operational lifecycle.</p>
</div>
<div class="flow-grid">
<article class="flow-step">
<span class="flow-step-number">1</span>
<h3>Choose a server</h3>
<p>Browse the supported game catalog, confirm the available plan, and select a region that matches your community.</p>
</article>
<article class="flow-step">
<span class="flow-step-number">2</span>
<h3>Configure it</h3>
<p>Use the GSP panel to set startup parameters, edit configs, install supported content, and keep backups or updates under control.</p>
</article>
<article class="flow-step">
<span class="flow-step-number">3</span>
<h3>Manage through GSP</h3>
<p>Operate the service through the panel for lifecycle, monitoring, logs, file access, and routine administrative work.</p>
</article>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="panel-preview">
<div class="panel-preview-shell">
<img src="<?= website_escape(website_asset('images/dark.jpg')) ?>" alt="GSP control panel preview background">
</div>
<div class="panel-preview-stats">
<div class="section-heading">
<h2>GSP control panel</h2>
<p>Customers do not get a fake storefront-only experience. They manage hosted servers through the same GSP platform that handles starts, stops, files, logs, addons, scheduling, and provisioning workflows.</p>
</div>
<div class="summary-card">
<h3>Operational access</h3>
<p>Start and stop servers, manage files, inspect logs, update Workshop content, and adjust configuration safely from the panel.</p>
</div>
<div class="stack-actions">
<a class="button button-primary" href="<?= website_escape(panel_url()) ?>">Open Control Panel</a>
<a class="button button-secondary" href="<?= website_escape(website_url('docs.php')) ?>">Read Documentation</a>
</div>
</div>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="summary-grid">
<article class="summary-card">
<h3>Documentation and support</h3>
<p>Game-specific docs, panel guidance, and troubleshooting live in the same repo and can be served without depending on billing configuration.</p>
<div class="card-actions">
<a class="button button-secondary" href="<?= website_escape(website_url('docs.php')) ?>">Documentation</a>
<a class="button button-ghost" href="<?= website_escape(website_url('support.php')) ?>">Support</a>
</div>
</article>
<article class="summary-card">
<h3>Panel and login routing</h3>
<p>The Control Panel and Login actions are configuration-driven. The public site no longer assumes a fragile `/panel/` path under the marketing domain.</p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(login_url()) ?>">Login</a>
<a class="button button-secondary" href="<?= website_escape(panel_url()) ?>">Control Panel</a>
</div>
</article>
</div>
</div>
</section>
<section class="cta-band">
<div class="container">
<div class="cta-panel">
<div>
<h2>Ready to deploy a server?</h2>
<p>Browse the current game catalog, confirm the right location, and manage the result through GSP.</p>
</div>
<div class="stack-actions">
<a class="button button-primary" href="<?= website_escape(website_url('serverlist.php')) ?>">View Game Servers</a>
<a class="button button-secondary" href="<?= website_escape(panel_url()) ?>">Open Control Panel</a>
<?php if ($discordUrl !== ''): ?>
<a class="button button-ghost" href="<?= website_escape($discordUrl) ?>" target="_blank" rel="noopener noreferrer">Discord</a>
<?php endif; ?>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
$locations = is_array($locations) ? $locations : [];
?>
<section class="page-heading">
<div class="container">
<h1>Locations</h1>
<p>Deployment regions are configured data. Large or latency-sensitive communities should choose a region that matches where the players actually are, not just where the cheapest machine happened to be.</p>
</div>
</section>
<section class="section">
<div class="container">
<div class="location-grid">
<?php foreach ($locations as $location): ?>
<article class="location-card">
<h3><?= website_escape((string)($location['name'] ?? '')) ?></h3>
<p><?= website_escape((string)($location['region'] ?? '')) ?></p>
<small><?= website_escape((string)($location['host'] ?? '')) ?></small>
</article>
<?php endforeach; ?>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="summary-grid">
<article class="summary-card">
<h3>Regional fit matters</h3>
<p>Pick a region based on player distribution and the specific game. Older titles and mod-heavy servers can be more sensitive to latency and disk performance than the average commodity hosting page admits.</p>
</article>
<article class="summary-card">
<h3>Need a different region?</h3>
<p>If the catalog does not list the region you need yet, contact support. The site copy now makes this explicit instead of hiding the conversation behind a dead link.</p>
</article>
</div>
</div>
</section>

View file

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
?>
<section class="page-heading">
<div class="container">
<h1>Pricing</h1>
<p>Gameservers.World pricing is catalog-driven. The public site does not hardcode fake plan availability; it shows live service rows when the billing catalog is available and falls back to guidance when it is not.</p>
</div>
</section>
<section class="section">
<div class="container">
<div class="summary-grid">
<article class="summary-card">
<h3>What you are paying for</h3>
<p>Dedicated resources, full configuration access, mod and Workshop support where available, control through GSP, backups, and support from people who know the game hosting side of the product.</p>
</article>
<article class="summary-card">
<h3>How pricing is presented</h3>
<p>Plan data comes from the billing catalog. That keeps the marketing site aligned with the actual services table instead of drifting into stale hardcoded prices.</p>
</article>
</div>
</div>
</section>
<section class="section">
<div class="container">
<?php if (!empty($services)): ?>
<div class="service-grid">
<?php foreach ($services as $service): ?>
<?php
$serviceName = trim((string)($service['cfg_game_name'] ?? $service['service_name'] ?? 'Game Server'));
$price = (float)($service['price_monthly'] ?? 0);
?>
<article class="service-card">
<h3><?= website_escape($serviceName) ?></h3>
<p><?= website_escape(trim((string)($service['description'] ?? '')) ?: 'Dedicated hosting with full configuration access.') ?></p>
<div class="section-divider"></div>
<div class="service-price"><?= $price > 0 ? '$' . number_format($price, 2) . ' / month' : 'Contact for pricing' ?></div>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(billing_url('order.php?service_id=' . rawurlencode((string)$service['service_id']))) ?>">Order</a>
<a class="button button-secondary" href="<?= website_escape(website_url('serverlist.php')) ?>">Catalog</a>
</div>
</article>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="alert info">
<?= website_escape((string)website_config('admin_notice')) ?>
</div>
<?php endif; ?>
</div>
</section>

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
$discordUrl = trim((string)website_config('discord_url', ''));
$supportUrl = trim((string)website_config('support_url', ''));
$supportEmail = trim((string)website_config('support_email', ''));
$loginUnavailable = $loginUnavailable ?? false;
?>
<section class="page-heading">
<div class="container">
<h1>Support</h1>
<p>Documentation should handle the routine cases. Support exists for the ones that are still operationally messy: mods, legacy engines, provisioning issues, and the inevitable edge cases around community-maintained titles.</p>
</div>
</section>
<section class="section">
<div class="container">
<?php if ($loginUnavailable): ?>
<div class="alert">
Login URL is not configured for this deployment. Set <code>login_url</code> in the website config to route customers to the active panel login.
</div>
<?php endif; ?>
<div class="support-grid">
<article class="support-card">
<h3>Customer login</h3>
<p>Access existing servers, files, logs, and administrative workflows through the configured GSP panel login.</p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(login_url()) ?>">Login</a>
<a class="button button-secondary" href="<?= website_escape(panel_url()) ?>">Panel</a>
</div>
</article>
<article class="support-card">
<h3>Documentation</h3>
<p>Server guides, panel references, and troubleshooting content are available even if billing configuration is missing.</p>
<div class="card-actions">
<a class="button button-primary" href="<?= website_escape(website_url('docs.php')) ?>">Open Docs</a>
</div>
</article>
<article class="support-card">
<h3>Direct help</h3>
<p>Use Discord, a support portal, or email depending on what is configured for this deployment.</p>
<div class="card-actions">
<?php if ($discordUrl !== ''): ?>
<a class="button button-primary" href="<?= website_escape($discordUrl) ?>" target="_blank" rel="noopener noreferrer">Discord</a>
<?php endif; ?>
<?php if ($supportUrl !== ''): ?>
<a class="button button-secondary" href="<?= website_escape($supportUrl) ?>" target="_blank" rel="noopener noreferrer">Support Portal</a>
<?php endif; ?>
<?php if ($supportEmail !== ''): ?>
<a class="button button-ghost" href="mailto:<?= website_escape($supportEmail) ?>">Email</a>
<?php endif; ?>
</div>
</article>
</div>
</div>
</section>

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
website_render(
'pricing.php',
[
'activePage' => 'pricing',
'pageTitle' => 'Pricing - Gameservers.World',
'metaDescription' => 'Pricing guidance and available game server plans for Gameservers.World.',
'canonicalPath' => 'pricing.php',
'services' => website_fetch_services(),
]
);

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
$services = website_fetch_services();
website_render(
'game_servers.php',
[
'activePage' => 'servers',
'pageTitle' => 'Game Servers - Gameservers.World',
'metaDescription' => 'Browse supported game server packages, locations, and ordering options for Gameservers.World.',
'canonicalPath' => 'serverlist.php',
'services' => $services,
]
);

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
website_render(
'support.php',
[
'activePage' => 'support',
'pageTitle' => 'Support - Gameservers.World',
'metaDescription' => 'Support, Discord, documentation, and control panel access for Gameservers.World customers.',
'canonicalPath' => 'support.php',
]
);

View file

@ -42,6 +42,7 @@ This is the master module inventory for the Panel. Use it as the first stop befo
| [`user_admin`](user_admin.md) | User management | Production | Keep / Improve |
| [`user_games`](user_games.md) | Server provisioning and assignment | Production / functional | Keep / Improve |
| [`util`](util.md) | Miscellaneous utility tools | Functional / mixed | Keep / Rewrite selectively |
| [`website`](website.md) | Public Gameservers.World sales and documentation website | Functional / newly modularized | Keep / Improve |
## Shared Dependencies

59
docs/modules/website.md Normal file
View file

@ -0,0 +1,59 @@
# Website
Workspace reference: [`GSP-WORKSPACE.md`](../../../GSP-WORKSPACE.md)
## Purpose
Public Gameservers.World sales, documentation, and customer-entry website.
## Runtime Location
- Canonical public site: `Panel/modules/website/`
- Billing catalog and order flow: `Panel/modules/billing/`
- Legacy compatibility site: `Website/`
## Design Rules
- Public pages must load without fatal errors when billing config is missing
- Path and URL handling must be centralized
- Panel and login URLs must be configuration-driven
- Documentation must be readable without depending on billing database access
- Catalog pages may degrade gracefully when billing data is unavailable
## Helpers
The website module centralizes these helpers in `includes/bootstrap.php`:
- `website_url()`
- `website_asset()`
- `panel_url()`
- `login_url()`
- `billing_url()`
- `documentation_url()`
## Billing Interaction
The website does not include the billing config loader directly. It reads panel or billing DB values safely, uses them only when needed, and avoids public fatal errors tied to missing config files.
## Deployment
Recommended:
1. `DocumentRoot` -> `Panel/modules/website`
2. Apache `Alias /billing` -> `Panel/modules/billing`
3. Configure:
- `public_base_url`
- `billing_base_url`
- `panel_url`
- `login_url`
## Key Public Pages
- `index.php`
- `serverlist.php`
- `docs.php`
- `pricing.php`
- `locations.php`
- `support.php`
- `login.php`