feat: dynamic page titles + social media meta tags
Browser tab titles: - Protocol pages: 'Title · Site Name' - Collection pages: 'Title · Site Name' - User pages: '@username · Site Name' - Resets to site name on library/collections/about Social media previews (server-side, for crawlers): - index.php detects /protocols/slug, /collections/slug, /users/username - Looks up title + description from DB - Injects og:title, og:description, og:type, og:site_name, og:url - Injects twitter:card, twitter:title, twitter:description - Replaces default <title> with enriched version Frontend: - Default og/twitter meta tags on homepage - Path-based URLs (shared links) redirect to hash routes for SPA routing
This commit is contained in:
@@ -39,6 +39,13 @@ async function api(path, options = {}) {
|
||||
|
||||
// --- Init ---
|
||||
async function init() {
|
||||
// If path looks like /protocols/slug, /collections/slug, or /users/username
|
||||
// (shared link without hash), redirect to hash-based route
|
||||
var pathMatch = location.pathname.match(/^\/?(protocols|collections|users)\/([^\/]+)/);
|
||||
if (pathMatch && !location.hash) {
|
||||
history.replaceState(null, '', '#' + pathMatch[1] + '/' + pathMatch[2]);
|
||||
}
|
||||
|
||||
// Route immediately — don't wait for API calls
|
||||
route();
|
||||
window.addEventListener('hashchange', route);
|
||||
@@ -98,6 +105,7 @@ function navigate(view) {
|
||||
document.querySelectorAll('.mobile-panel a').forEach(a => a.classList.toggle('active', a.dataset.view === view));
|
||||
closeMenu();
|
||||
updateBreadcrumb(view);
|
||||
document.title = siteConfig.name || 'Protocol Droid';
|
||||
if (!isRouting) {
|
||||
if (view === 'library') location.hash = 'library';
|
||||
if (view === 'collections') location.hash = 'collections';
|
||||
@@ -329,6 +337,7 @@ async function showDetail(slug) {
|
||||
|
||||
try {
|
||||
const p = await api('/protocols/' + slug);
|
||||
document.title = (p.title || slug) + ' · ' + (siteConfig.name || 'Protocol Droid');
|
||||
document.getElementById('detailContent').innerHTML = renderDetail(p);
|
||||
renderDetailActions(p);
|
||||
} catch (e) {
|
||||
@@ -730,6 +739,7 @@ async function showCollectionDetail(slug) {
|
||||
|
||||
try {
|
||||
var c = await api('/collections/' + slug);
|
||||
document.title = (c.title || slug) + ' · ' + (siteConfig.name || 'Protocol Droid');
|
||||
var itemsHtml = '';
|
||||
for (var i = 0; i < (c.items || []).length; i++) {
|
||||
var item = c.items[i];
|
||||
@@ -802,6 +812,7 @@ async function showUser(username) {
|
||||
closeMenu();
|
||||
try {
|
||||
const data = await api('/users/' + username);
|
||||
document.title = '@' + (data.user.display_name || data.user.username) + ' · ' + (siteConfig.name || 'Protocol Droid');
|
||||
const p = data.protocols || [];
|
||||
const c = data.collections || [];
|
||||
document.getElementById('detailContent').innerHTML =
|
||||
|
||||
Reference in New Issue
Block a user