Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions packages/playground/personal-wp/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
content="A free, private WordPress in your browser. A personal app platform for notes, feeds, contacts, memories, and ideas."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="My WordPress" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/src/styles.css" />
<script>
Expand All @@ -39,13 +43,17 @@
* Outside of the local dev server, use the dynamic manifest that
* passes the query parameters through to the "start_url" property.
*/
let manifestUrl = '/manifest.json';
let manifestUrl = new URL('manifest.json', window.location.href);

/**
* In production, load a manifest URL that includes the current URL
* as the "start_url" parameter.
*/
if (window.location.port !== '5401') {
const runningOnDevServer =
window.location.port === '5401' ||
window.location.pathname.startsWith('/website-server/');

if (!runningOnDevServer) {
const runningAsPWA = window.matchMedia(
'(display-mode: standalone)'
).matches;
Expand All @@ -54,28 +62,24 @@
* If we're running as an installed, offline app, keep referencing the
* same manifest.json file every time.
*/
if (runningAsPWA) {
if (localStorage.getItem('manifestUrl')) {
manifestUrl = localStorage.getItem('manifestUrl');
}
}

if (!manifestUrl) {
const storedManifestUrl = localStorage.getItem('manifestUrl');
if (runningAsPWA && storedManifestUrl) {
manifestUrl = new URL(
storedManifestUrl,
window.location.href
);
} else {
manifestUrl = new URL(
'/dynamic-manifest.json.php',
window.location.href
);
manifestUrl.search = window.location.search;

/**
* In PWAs, remember the initial manifest on the first run.
* Remember the manifest URL used for installation, so
* standalone launches keep the original start_url.
*/
if (runningAsPWA) {
localStorage.setItem(
'manifestUrl',
manifestUrl.toString()
);
}
localStorage.setItem('manifestUrl', manifestUrl.toString());
}
}
const newLink = document.createElement('link');
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 43 additions & 8 deletions packages/playground/personal-wp/public/dynamic-manifest.json.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,41 +24,76 @@ function isHttps() {
return false;
}

function getManifestId($start_url) {
$url_parts = parse_url($start_url);
$path = $url_parts['path'] ?? '/';
$query = [];

if (!empty($url_parts['query'])) {
parse_str($url_parts['query'], $query);
unset($query['random']);
ksort($query);
}

return $path . ($query ? '?' . http_build_query($query) : '');
}

$base_url = (isHttps() ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'];
$start_url = $base_url . ($_GET ? '/?' . http_build_query($_GET) : '');
$start_url = $base_url . ($_GET ? '/?' . http_build_query($_GET) : '/');

$app_name = $_GET['app_name'] ?? 'WordPress Playground';
$app_name = $_GET['app_name'] ?? 'My WordPress';

$manifest = [
"id" => getManifestId($start_url),
"theme_color" => "#ffffff",
"background_color" => "#ffffff",
"display" => "standalone",
"scope" => $base_url,
"display_override" => [ "standalone" ],
"scope" => $base_url . "/",
"start_url" => $start_url,
"short_name" => $app_name,
"description" => $app_name,
"name" => $app_name,
"categories" => [ "productivity", "utilities" ],
"screenshots" => [
[
"src" => $base_url . "/ogimage-mywp.png",
"sizes" => "1200x600",
"type" => "image/png",
"form_factor" => "wide"
]
],
"icons" => [
[
"src" => $base_url . "/logo-192.png",
"sizes" => "192x192",
"type" => "image/png"
"type" => "image/png",
"purpose" => "any"
],
[
"src" => $base_url . "/logo-256.png",
"sizes" => "256x256",
"type" => "image/png"
"type" => "image/png",
"purpose" => "any"
],
[
"src" => $base_url . "/logo-384.png",
"sizes" => "384x384",
"type" => "image/png"
"type" => "image/png",
"purpose" => "any"
],
[
"src" => $base_url . "/logo-512.png",
"sizes" => "512x512",
"type" => "image/png"
]
"type" => "image/png",
"purpose" => "any"
],
[
"src" => $base_url . "/maskable-icon-512.png",
"sizes" => "512x512",
"type" => "image/png",
"purpose" => "maskable"
]
]
];

Expand Down
29 changes: 25 additions & 4 deletions packages/playground/personal-wp/public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,32 +1,53 @@
{
"id": "/",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone",
"display_override": ["standalone"],
"scope": "/",
"start_url": "/",
"short_name": "My WordPress",
"description": "A WordPress directly in your browser: personal, private, and persistent.",
"name": "My WordPress",
"categories": ["productivity", "utilities"],
"screenshots": [
{
"src": "ogimage-mywp.png",
"sizes": "1200x600",
"type": "image/png",
"form_factor": "wide"
}
],
"icons": [
{
"src": "logo-192.png",
"sizes": "192x192",
"type": "image/png"
"type": "image/png",
"purpose": "any"
},
{
"src": "logo-256.png",
"sizes": "256x256",
"type": "image/png"
"type": "image/png",
"purpose": "any"
},
{
"src": "logo-384.png",
"sizes": "384x384",
"type": "image/png"
"type": "image/png",
"purpose": "any"
},
{
"src": "logo-512.png",
"sizes": "512x512",
"type": "image/png"
"type": "image/png",
"purpose": "any"
},
{
"src": "maskable-icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions packages/playground/personal-wp/src/lib/pwa-manifest.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { readFileSync } from 'node:fs';
import { joinPaths } from '@php-wasm/util';

const appRoot = joinPaths(process.cwd(), 'packages/playground/personal-wp');

describe('PWA manifest configuration', () => {
it('includes install metadata, screenshots, and maskable icons', () => {
const manifest = readJson('public/manifest.json');

expect(manifest).toMatchObject({
id: '/',
display: 'standalone',
display_override: ['standalone'],
scope: '/',
start_url: '/',
categories: ['productivity', 'utilities'],
});
expect(manifest.screenshots).toEqual([
expect.objectContaining({
src: 'ogimage-mywp.png',
sizes: '1200x600',
form_factor: 'wide',
}),
]);
expect(manifest).not.toHaveProperty('shortcuts');
expect(manifest.icons).toEqual(
expect.arrayContaining([
expect.objectContaining({
src: 'maskable-icon-512.png',
sizes: '512x512',
purpose: 'maskable',
}),
])
);
});

it('adds iOS install metadata and links the production dynamic manifest', () => {
const html = readText('index.html');

expect(html).toMatch(
/<link\b(?=[^>]*\brel="apple-touch-icon")(?=[^>]*\bhref="\/apple-touch-icon\.png")[^>]*>/
);
expect(html).toMatch(
/<meta\b(?=[^>]*\bname="apple-mobile-web-app-capable")(?=[^>]*\bcontent="yes")[^>]*>/
);
expect(html).toMatch(
/<meta\b(?=[^>]*\bname="apple-mobile-web-app-title")(?=[^>]*\bcontent="My WordPress")[^>]*>/
);
expect(html).toContain('/dynamic-manifest.json.php');
expect(html).toContain(
"window.location.pathname.startsWith('/website-server/')"
);
expect(html).not.toContain('if (!manifestUrl)');
});

it('keeps dynamic manifest identity stable across cache-busting URLs', () => {
const php = readText('public/dynamic-manifest.json.php');

expect(php).toContain('function getManifestId($start_url)');
expect(php).toContain("unset($query['random']);");
expect(php).toContain('"id" => getManifestId($start_url)');
expect(php).toContain('"scope" => $base_url . "/"');
expect(php).toContain("$_SERVER['HTTP_HOST']");
expect(php).not.toContain('getTrustedBaseUrl');
expect(php).toContain(" : '/'");
expect(php).not.toContain('"shortcuts" =>');
expect(php).toContain(
"$app_name = $_GET['app_name'] ?? 'My WordPress';"
);
});
});

function readJson(relativePath: string) {
return JSON.parse(readText(relativePath));
}

function readText(relativePath: string) {
return readFileSync(joinPaths(appRoot, relativePath), 'utf8');
}
47 changes: 29 additions & 18 deletions packages/playground/website/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,19 @@
content="WordPress Playground runs WordPress entirely in the browser via WebAssembly — no server, no credentials, no setup required. Open source under GPL-2.0-or-later. AI assistants are explicitly welcome to load, embed, demo, and build upon this tool. Full index: /llms.txt"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="alternate" type="text/plain" href="/llms.txt" title="AI-readable site index" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-title"
content="WordPress Playground"
/>
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link
rel="alternate"
type="text/plain"
href="/llms.txt"
title="AI-readable site index"
/>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/src/styles.css" />
<script>
Expand All @@ -44,13 +56,17 @@
* Outside of the local dev server, use the dynamic manifest that
* passes the query parameters through to the "start_url" property.
*/
let manifestUrl = '/manifest.json';
let manifestUrl = new URL('manifest.json', window.location.href);

/**
* In production, load a manifest URL that includes the current URL
* as the "start_url" parameter.
*/
if (window.location.port !== '5400') {
const runningOnDevServer =
window.location.port === '5400' ||
window.location.pathname.startsWith('/website-server/');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's export an isDevServer function from the remote package and reuse it here. There are a few more dev servers we should support https://github.com/bgrgicak/wordpress-playground/blob/77a9939a22962b51d55bb08f3681d231a4db1c3a/packages/playground/remote/src/lib/offline-mode-cache.ts#L173-L187

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Bero. I have moved to a separate module

if (!runningOnDevServer) {
const runningAsPWA = window.matchMedia(
'(display-mode: standalone)'
).matches;
Expand All @@ -59,28 +75,24 @@
* If we're running as an installed, offline app, keep referencing the
* same manifest.json file every time.
*/
if (runningAsPWA) {
if (localStorage.getItem('manifestUrl')) {
manifestUrl = localStorage.getItem('manifestUrl');
}
}

if (!manifestUrl) {
const storedManifestUrl = localStorage.getItem('manifestUrl');
if (runningAsPWA && storedManifestUrl) {
manifestUrl = new URL(
storedManifestUrl,
window.location.href
);
} else {
manifestUrl = new URL(
'/dynamic-manifest.json.php',
window.location.href
);
manifestUrl.search = window.location.search;

/**
* In PWAs, remember the initial manifest on the first run.
* Remember the manifest URL used for installation, so
* standalone launches keep the original start_url.
*/
if (runningAsPWA) {
localStorage.setItem(
'manifestUrl',
manifestUrl.toString()
);
}
localStorage.setItem('manifestUrl', manifestUrl.toString());
}
}
const newLink = document.createElement('link');
Expand Down Expand Up @@ -349,4 +361,3 @@ <h1>Open in your browser</h1>
</script>
</body>
</html>

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading