@prefix sioc: <http://rdfs.org/sioc/ns#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix content: <http://purl.org/rss/1.0/modules/content/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://lhero.org/portfolio/lh-web-application/>
  a sioc:Post ;
  dc:title "Architecture" ;
  dcterms:identifier 151541 ;
  dc:modified "2026-10-19T15:37:10Z"^^xsd:dateTime ;
  dc:created "2026-10-17T17:31:10Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-web-application/architecture/> ;
  sioc:has_creator <https://lhero.org/author/1/#account> ;
  sioc:has_container <https://lhero.org/#posts> ;
  content:encoded "<ul class=\"lh_portfolio-meta\"><li><strong>Part of:</strong> <a href=\"https://lhero.org/portfolio/lh-web-application/\">LH Web Application</a></li></ul><h2 class=\"wp-block-heading\">Front end</h2><ul class=\"wp-block-list\"><li>The head gets the viewport meta, <code>theme-color</code>, the manifest link, Apple touch icons (resized from the chosen attachment), the Apple web app tags when that setting is on, and <code>lh-web-application-user</code>: a short hash of the signed-in user ID, empty when logged out.</li><li>iOS startup images: one link per current iPhone screen (portrait) and iPad screen (both orientations), each with a <code>media</code> query for that screen, since iOS shows no startup screen without an image of exactly its size. Through an image CDN built on Jetpack&#8217;s (LH Image CDN), each is an exact-size crop of the original upload rather than WordPress&#8217;s scaled-down copy; the CDN never enlarges, so screens bigger than the original get no link. Served locally, a screen only gets a link when a crop of exactly its size exists.</li><li>Two rewrite endpoints: <code>/pwa-manifest.json</code> and <code>/serviceworker.script</code>. The service worker is generated per site and served without authentication, so LiteSpeed can cache it; purge it after changing anything it contains.</li><li>The manifest takes <code>name</code>, <code>description</code> (the tagline, left out when empty, both as plain text), <code>lang</code> and <code>dir</code> from the site settings, with <code>scope</code> <code>/</code> and an <code>id</code> hashed from the site. Browsers identify an installed app by its <code>id</code>, so how it is built must never change.</li><li>A virtual offline page at <code>/offline/</code> showing the offline message. The <code>offline</code> page slug is reserved.</li><li>The manifest <code>start_url</code> is the home page with <code>?lh_web_application-start_url=1</code>, precached so the installed app opens offline.</li><li>Scripts are registered with core WordPress APIs (the main script as <code>lh_web_application-script</code>, deferred, in the footer); their data attributes are added on <code>script_loader_tag</code>.</li><li>The main script registers a daily periodic sync with the tag <code>update</code> where the browser supports it. The worker scripts of LH BuddyPress Live Content and LH Web Push Notifications handle it.</li></ul><h2 class=\"wp-block-heading\">Service worker caches</h2><ul class=\"wp-block-list\"><li><code>lh_web_application-offline-{hash}</code>: the offline page, precached on install. The name includes the offline message, so editing it makes the next worker refetch the page.</li><li><code>lh_web_application-pages-{hash}</code>: the start URL plus every same-origin HTML page viewed, network first. Kept however old, but capped at 300; when full, the least recently used page is dropped, never the start URL or the offline page. Never stored: wp-admin (including admin-ajax), <code>/wp-json/</code>, <code>?rest_route=</code>, <code>wp-login.php</code>, URLs with a nonce parameter, and responses marked <code>no-store</code>.</li><li>Navigation preload is turned on when a worker activates, so the browser starts fetching a page while the worker is still starting up; pages are served from that preloaded response, falling back to a normal fetch. Navigations the worker does not handle (wp-admin, login, the REST API, nonce URLs) are answered from the same preload, so they are not requested twice.</li><li><code>lh_web_application-images</code>: cache first and kept however old, but capped at 150; when full, the least recently used image is dropped. Cross-origin images (Photon, Gravatar) are fetched by the worker in CORS mode so they can be cached; hosts without CORS still load, uncached. Responses marked <code>no-store</code> or <code>private</code> are skipped.</li><li><code>lh_web_application-static</code>: cache first, precached with the scripts and styles the site registers. URLs carry <code>?ver=</code>, so a change is a new URL; files unused for 180 days are removed, so replaced versions age out.</li><li>The worker records when each static file, image and page was last used in an IndexedDB database, <code>lh_web_application</code> (at most one write per entry per day), and sweeps those caches when a new worker activates and at most once a day after that. Without IndexedDB nothing breaks: static files simply stay, and the images and pages caches drop the oldest added instead.</li><li>The page script clears the pages and images caches whenever the user marker changes: logout, session expiry or a different account.</li><li>Offline, a page served from the cache is marked with <code>lh-web-application-cached</code> (the time it was saved), and the page shows a status bar and adds the body class <code>lh_web_application-served-from-cache</code>. The bar&#8217;s inline styling has zero specificity, so themes can restyle it.</li><li>Activating a new worker deletes only older <code>lh_web_application-*</code> caches.</li></ul><h2 class=\"wp-block-heading\">Settings and data</h2><ul class=\"wp-block-list\"><li>Settings, Web Application (site admins, <code>manage_options</code>): short name, manifest icon (and whether it is maskable), iOS icon and startup image, display mode, Apple web app capable, shortcut menu, theme and background colours (hex), and the offline message. Stored per site in <code>lh_web_application-options</code>. Icons fall back to the site icon, the background colour to the theme&#8217;s; emptying a colour or the offline message clears it.</li><li>The shortcut menu becomes the manifest <code>shortcuts</code>; each of its items can have an icon (the Shortcut icon field on Appearance, Menus), stored in <code>_lh_web_application-menu_icon-attachment_id</code> post meta.</li><li>Cron: <code>lh_web_application_initial_run</code> once on activation (flushes rewrites, generates icon sizes) and <code>lh_web_application_ping_single_url</code>, which rewarms the service worker, manifest, start URL and offline page after a page cache clear.</li><li>Deleting the plugin removes its settings, scheduled events and menu icon meta from every site. Uploaded images are left alone.</li></ul><h2 class=\"wp-block-heading\">Code structure</h2><p class=\"wp-block-paragraph\">Since 1.33.0 each job has its own class in <code>includes/</code>, loaded by <code>plugin_init()</code> behind the <code>lh_web_application_init_plugin</code> filter. Every class extends <code>LH_web_application_plugin</code> and is a singleton that hooks itself in its constructor.</p><ul class=\"wp-block-list\"><li><code>lh-web-application.php</code> (<code>LH_web_application_plugin</code>): identity (namespace, text domain, version from the header, file and asset paths), <code>write_log()</code>, <code>plugin_init()</code>, the <code>script_loader_tag</code> filter, and activation, deactivation (scheduling and clearing the cron events) and uninstall.</li><li>Three traits, used by the main class and loaded before it, so their methods stay callable on <code>LH_web_application_plugin</code> and every child class: <code>LH_web_application_options_trait</code> (option, field and endpoint names, and the settings readers), <code>LH_web_application_images_trait</code> (icon and startup image sizes, including <code>create_apple_startup_sizes()</code>, and the image URL helpers) and <code>LH_web_application_scripts_trait</code> (script tag attributes shared by every class, and <code>get_script_src_by_handle()</code>).</li><li><code>LH_web_application_manifest_class</code>: the manifest JSON and shortcuts, and the <code>/pwa-manifest.json</code> endpoint.</li><li><code>LH_web_application_service_worker_class</code>: the <code>/serviceworker.script</code> endpoint (printed by <code>partials/sw4.php</code>), its unauthenticated request, the precache and import lists, and the page script <code>lh_web_application-script</code> with the fetch handler.</li><li><code>LH_web_application_head_meta_class</code>: the head tags, Apple touch icons, iOS startup images and the image sub-sizes they need.</li><li><code>LH_web_application_menu_icon_class</code>: the Shortcut icon field on Appearance, Menus.</li><li><code>LH_web_application_offline_page_class</code>: the virtual <code>/offline/</code> page, the reserved slug and the login URL filter.</li><li><code>LH_web_application_cron_class</code>: the initial run and the rewarm after a page cache clear.</li><li><code>LH_web_application_settings_class</code>: Settings, Web Application.</li><li><code>LH_web_application_app_install_class</code>: the install sheet printed in the footer and its script <code>assets/scripts/app_install-boot.js</code>, the Install App menu item, the snooze admin-ajax action and the <code></code> shortcode.</li><li><code>LH_web_application_install_block_class</code>: the Install the App block, registered from <code>blocks/install-button/</code> (block.json, a no-build editor script and a small stylesheet) and rendered on the server.</li><li><code>libraries/lh-admin-ajax-noncer-class.php</code> provides the <code>return_wp_nonce</code> admin-ajax action.</li></ul>"^^rdf:XMLLiteral ;
  sioc:content "Part of: LH Web ApplicationFront endThe head gets the viewport meta, theme-color, the manifest link, Apple touch icons (resized from the chosen attachment), the Apple web app tags when that setting is on, and lh-web-application-user: a short hash of the signed-in user ID, empty when logged out.iOS startup images: one link per current iPhone screen (portrait) and iPad screen (both orientations), each with a media query for that screen, since iOS shows no startup screen without an image of exactly its size. Through an image CDN built on Jetpack&#8217;s (LH Image CDN), each is an exact-size crop of the original upload rather than WordPress&#8217;s scaled-down copy; the CDN never enlarges, so screens bigger than the original get no link. Served locally, a screen only gets a link when a crop of exactly its size exists.Two rewrite endpoints: /pwa-manifest.json and /serviceworker.script. The service worker is generated per site and served without authentication, so LiteSpeed can cache it; purge it after changing anything it contains.The manifest takes name, description (the tagline, left out when empty, both as plain text), lang and dir from the site settings, with scope / and an id hashed from the site. Browsers identify an installed app by its id, so how it is built must never change.A virtual offline page at /offline/ showing the offline message. The offline page slug is reserved.The manifest start_url is the home page with ?lh_web_application-start_url=1, precached so the installed app opens offline.Scripts are registered with core WordPress APIs (the main script as lh_web_application-script, deferred, in the footer); their data attributes are added on script_loader_tag.The main script registers a daily periodic sync with the tag update where the browser supports it. The worker scripts of LH BuddyPress Live Content and LH Web Push Notifications handle it.Service worker cacheslh_web_application-offline-{hash}: the offline page, precached on install. The name includes the offline message, so editing it makes the next worker refetch the page.lh_web_application-pages-{hash}: the start URL plus every same-origin HTML page viewed, network first. Kept however old, but capped at 300; when full, the least recently used page is dropped, never the start URL or the offline page. Never stored: wp-admin (including admin-ajax), /wp-json/, ?rest_route=, wp-login.php, URLs with a nonce parameter, and responses marked no-store.Navigation preload is turned on when a worker activates, so the browser starts fetching a page while the worker is still starting up; pages are served from that preloaded response, falling back to a normal fetch. Navigations the worker does not handle (wp-admin, login, the REST API, nonce URLs) are answered from the same preload, so they are not requested twice.lh_web_application-images: cache first and kept however old, but capped at 150; when full, the least recently used image is dropped. Cross-origin images (Photon, Gravatar) are fetched by the worker in CORS mode so they can be cached; hosts without CORS still load, uncached. Responses marked no-store or private are skipped.lh_web_application-static: cache first, precached with the scripts and styles the site registers. URLs carry ?ver=, so a change is a new URL; files unused for 180 days are removed, so replaced versions age out.The worker records when each static file, image and page was last used in an IndexedDB database, lh_web_application (at most one write per entry per day), and sweeps those caches when a new worker activates and at most once a day after that. Without IndexedDB nothing breaks: static files simply stay, and the images and pages caches drop the oldest added instead.The page script clears the pages and images caches whenever the user marker changes: logout, session expiry or a different account.Offline, a page served from the cache is marked with lh-web-application-cached (the time it was saved), and the page shows a status bar and adds the body class lh_web_application-served-from-cache. The bar&#8217;s inline styling has zero specificity, so themes can restyle it.Activating a new worker deletes only older lh_web_application-* caches.Settings and dataSettings, Web Application (site admins, manage_options): short name, manifest icon (and whether it is maskable), iOS icon and startup image, display mode, Apple web app capable, shortcut menu, theme and background colours (hex), and the offline message. Stored per site in lh_web_application-options. Icons fall back to the site icon, the background colour to the theme&#8217;s; emptying a colour or the offline message clears it.The shortcut menu becomes the manifest shortcuts; each of its items can have an icon (the Shortcut icon field on Appearance, Menus), stored in _lh_web_application-menu_icon-attachment_id post meta.Cron: lh_web_application_initial_run once on activation (flushes rewrites, generates icon sizes) and lh_web_application_ping_single_url, which rewarms the service worker, manifest, start URL and offline page after a page cache clear.Deleting the plugin removes its settings, scheduled events and menu icon meta from every site. Uploaded images are left alone.Code structureSince 1.33.0 each job has its own class in includes/, loaded by plugin_init() behind the lh_web_application_init_plugin filter. Every class extends LH_web_application_plugin and is a singleton that hooks itself in its constructor.lh-web-application.php (LH_web_application_plugin): identity (namespace, text domain, version from the header, file and asset paths), write_log(), plugin_init(), the script_loader_tag filter, and activation, deactivation (scheduling and clearing the cron events) and uninstall.Three traits, used by the main class and loaded before it, so their methods stay callable on LH_web_application_plugin and every child class: LH_web_application_options_trait (option, field and endpoint names, and the settings readers), LH_web_application_images_trait (icon and startup image sizes, including create_apple_startup_sizes(), and the image URL helpers) and LH_web_application_scripts_trait (script tag attributes shared by every class, and get_script_src_by_handle()).LH_web_application_manifest_class: the manifest JSON and shortcuts, and the /pwa-manifest.json endpoint.LH_web_application_service_worker_class: the /serviceworker.script endpoint (printed by partials/sw4.php), its unauthenticated request, the precache and import lists, and the page script lh_web_application-script with the fetch handler.LH_web_application_head_meta_class: the head tags, Apple touch icons, iOS startup images and the image sub-sizes they need.LH_web_application_menu_icon_class: the Shortcut icon field on Appearance, Menus.LH_web_application_offline_page_class: the virtual /offline/ page, the reserved slug and the login URL filter.LH_web_application_cron_class: the initial run and the rewarm after a page cache clear.LH_web_application_settings_class: Settings, Web Application.LH_web_application_app_install_class: the install sheet printed in the footer and its script assets/scripts/app_install-boot.js, the Install App menu item, the snooze admin-ajax action and the  shortcode.LH_web_application_install_block_class: the Install the App block, registered from blocks/install-button/ (block.json, a no-build editor script and a small stylesheet) and rendered on the server.libraries/lh-admin-ajax-noncer-class.php provides the return_wp_nonce admin-ajax action." ;
  sioc:topic <https://lhero.org/?taxonomy=author&term=cap-1> .

<https://lhero.org/author/1/#account> rdfs:seeAlso <https://lhero.org/author/1/?feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=author&term=cap-1> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1&feed=lhrdf&format=turtle> .
