Back to Logbook
May 23, 2026 Project: #Portfolio

Six Patches: SEO Overhaul, Social Automation, Authority Pages, Session Timeout

focused

Six patches across one day. This was a long one.

Session Idle Timeout + Admin Bug Fixes

Admin idle timeout is now enforced at 30 minutes via a last_activity session key in AdminAuthFilter. Found a subtle bug with the destroy-before-flash pattern: session()->destroy() wipes the session before the flash message can be stored, so the timeout redirect was showing a blank login page with no explanation. Fixed to session()->remove([...auth keys...]) to keep the session alive long enough to carry the flash to the login page.

Settings page was redirecting to the homepage on the live site. Root cause: echo view() instead of return view() — the response was being echoed directly, bypassing CI4's response pipeline. About page canonical was pointing to /profile instead of /about. Both fixed.

SEO Overhaul

Per-page Open Graph tags across Posts, Works, Logbook, and Profile — each page now passes its own og_title, og_description, og_type, and og_image to the layout. og:url was always emitting the homepage URL regardless of the current page — now uses the per-page canonical. JSON-LD type is dynamic: WebSite on homepage, Article on detail pages, Person on About. JSON_HEX_TAG added to prevent a </script> substring in JSON-LD from breaking out of the script block. Profile meta now uses a clean bio (strip_tags before use as og:description). Added public/llms.txt following the llmstxt.org spec for AI crawler guidance.

Dynamic llms.txt and hreflang

Static llms.txt replaced with a Llms controller pulling all published works and featured posts from the database. New is_llms_featured column on the posts table — "Feature in llms.txt" checkbox in admin Blog. Only flagged posts appear. hreflang x-default now points to the bare domain; language alternates use ?lang={code} on the current page URL so search engine crawlers get correctly translated content without needing a session cookie. get_current_language() reads ?lang= first and persists to session.

Cover Image, OG Fallback Chain, Share Buttons

Admin Blog create/edit now includes a cover image upload with live preview and remove-checkbox. The OG image fallback chain throughout the frontend: uploaded cover image → global OG image from settings → gradient placeholder. file_exists() guard on cover image paths so stale database values fall through to the next fallback instead of rendering a broken img tag. Share buttons (X/Twitter, LinkedIn, Copy Link) added to post and work detail pages — no external scripts, brand colours, translated labels in all 12 language files.

Authority Pages: /now, /stack, /ai-lab

Three new public pages backed by admin-editable content. /now shows what I'm working on right now (single-row upsert, Summernote editor, auto-translated). /stack is a technology stack grouped by category with sort order. /ai-lab lists AI experiments and tools with status badges and demo links. Full CRUD in the admin via a MetaPages controller. Footer redesigned as a five-column inline grid with a double-width intro column, proper mobile collapse. Stack and AI Lab sections appended to /llms.txt. cron.sh added at project root for scheduler and queue targets with logging to writable/logs/cron.log.

Full Social Automation: Real Posts + RAG Comment and DM Replies

The demo stubs are gone. PublishSocialPosts rewritten with live API calls for all four platforms: Facebook Graph API v25.0, LinkedIn UGC Posts API, Twitter API v2 with 280-char truncation, Instagram two-step container and publish flow.

CheckSocialComments command now polls all published posts for new comments across Facebook, Instagram, Twitter mentions, and LinkedIn. Deduplicates by platform comment ID, classifies with RAG, auto-posts reply, saves leads scoring 40 or above to social_leads. Same pipeline for DMs via CheckSocialDms across Facebook Messenger, Instagram DMs, and Twitter DMs.

The SocialRagService pipeline: embed incoming message → cosine-score KB documents → top-5 by similarity → feed context + message to AI → parse JSON response (classification, lead score 0–100, suggested reply). Falls back to keyword search when embeddings are not configured. social_dms table added with a unique index on message_id to prevent duplicate processing.

Social Post Edit and Reschedule

Any draft, scheduled, or failed post can now be edited — content, account, scheduled time, link URL. Saving with a date moves it to scheduled; without a date it stays draft. Failed posts get a dedicated reschedule action that resets retry count, clears the error message, and queues it for the next cron run. The failed section in the queue view shows last error message and retry badge count prominently at the top. Guards added: editing a published post is rejected, rescheduling a non-failed post is rejected.

Lessons Learned

The session destroy-before-flash bug is a CI4 footgun: destroy() wipes everything including queued flash data. Always use remove() with specific keys when you want the session to survive to carry a flash message. The og:url-always-homepage bug is the kind of thing that passes visual testing because the content looks right — only visible when you inspect the source or check sharing previews.