News
Overview
The News module manages articles, publications, and announcements displayed on the public landing page. It is a full CMS: article categories, localized (bilingual) content, rich-text editing, SEO meta tags, featured thumbnails, a live device preview, reader analytics, and a personalized recommendation engine.
The module has two sides:
- Admin panel — Filament cluster at
/portal/news(navigation label News, group Master). - Public site — the
/articleslanding pages plus analytics/API endpoints.
Key Entities
| Entity | Description |
|---|---|
| Article | Published news content — localized title/content/excerpt, slug, tags, category, school, author, thumbnail, SEO meta, publish date, active flag |
| ArticleCategory | Category grouping for articles — localized name/description, slug, active flag |
| ArticleAnalytic | Reading-session analytics per article — fingerprint, duration, scroll depth, completion, device, geo location |
Admin Panel (/portal/news)
The cluster contains three areas:
| Area | URL | Purpose |
|---|---|---|
| Articles | /portal/news/articles | Manage article records (list, create, edit, view) |
| Article Categories | /portal/news/article-categories | Manage categories |
| Popular Articles | /portal/news/popular-articles-page | Reader analytics dashboard |
1. Articles — List Page
Columns
| Column | Notes |
|---|---|
| Name (title) | Searchable, localized |
| School | Shows school name or General placeholder |
| Category | Category name or - |
| Slug | Searchable |
| Author | Searchable, - if empty |
| Status | Badge — Active (green) / Inactive (gray) |
Filters
- Status — ternary filter: Active / Inactive
- School — select with General option
- Category — select from existing categories
- Published Date — date range (from / until)
- Created Date — date range (from / until)
Global search uses full-text search (Meilisearch) — matches title, content, and excerpt in both languages, plus author and tags, with relevance ranking.
Row actions: View, Edit. Bulk action: Delete (selected rows).
2. Articles — Create / Edit Form
The form is split into a General / Localized content column (span 3) and a Thumbnail / SEO sidebar (span 1).
General section
| Field | Required | Notes |
|---|---|---|
| School (optional) | No | Default General — no school |
| Category | No | Select with + create option (inline create: slug + localized name/description) |
| Slug | Yes | Must be unique (validated) — e.g. registration-guide |
| Author | No | Free text, e.g. Admin |
| Published At | No | Date picker, min date = today (scheduling) |
| Tags | No | Comma/enter-separated tag input |
Localized content (tab per language, EN/ID)
| Field | Required (base locale) | Notes |
|---|---|---|
| Name (title) | Yes | Plain text |
| Excerpt | Yes | Textarea, rendered as an HTML preview on the public detail page |
| Content | Yes | Headless rich-text editor (headings, quotes, media, / quick menu) |
The localization tab bar provides an Auto-translate button that copies the base-language content into the current language.
Thumbnail section
| Field | Notes |
|---|---|
| Upload Image | Image upload (public disk, articles/ dir, image editor), stored as an Asset, preview via ImageKit |
| Alt text | Max 150 chars, for accessibility |
SEO section
- Meta tags repeater (modal) — list of
{name, content}pairs. nameis a select of predefined SEO keys (e.g.description,keywords,og:title,og:image,twitter:...) with + create option for custom names; each name can only be used once (validated byUniqueSeoName).contentfree text, max 255 chars.
3. Articles — View Page
Displays:
- General info — slug, category, school, author, status badge, tags
- Localized values — title, excerpt (rendered as prose/HTML preview), content (rendered from the rich-text editor)
- Live Preview card — an iframe of the public article page with Desktop / Tablet / Mobile device switcher, framed mockups, responsive scaling, and loading state.
4. Article Categories — Manage Page
Form
| Field | Required | Notes |
|---|---|---|
| Slug | Yes | Unique, max 100 chars |
| Active | No | Switcher, default on |
| Name (localized) | Yes | Max 255 chars |
| Description (localized) | No | Textarea |
Table columns: Name, Slug (badge, searchable/sortable), Articles (count per category, sortable), Active (boolean icon), Created Date (sortable, hidden by default, toggleable).
Row actions: Edit, Delete. Sorting default: by slug.
Only Active categories appear on the public site.
5. Popular Articles — Analytics Dashboard
- Stats cards (poll every 60 s): Total Article Categories, Unpublished Articles (null or future
published_at), Published Articles (published_at <= now). - Reader Geography — treemap chart of reads by country (from IP geo-lookup; "Unknown" bucket).
- Popular Articles table — per article: Reads, Unique Readers, Total Time (humanized
45s/3m 20s/1h 5m), Completed. Default sort: total time desc. Pagination 5 / 10 / 25. Empty state when no analytics data exists yet.
Publication Flow
The following flowchart shows how an article moves from the admin form to the public site:
Key rules
- An article is publicly visible only when
is_active = trueandpublished_atis set and not in the future. - Setting a future
published_atschedules the article — it stays hidden (unpublished) until that date. - Every save invalidates the cached recommendation/analytics data so changes appear immediately.
Public Site
Routes
| Route | Page | Description |
|---|---|---|
GET /articles | Articles/Index | Browse + filter + search articles |
GET /articles/{slug} | Articles/Show | Article detail with recommendations |
GET /articles/{slug}/iframe | Articles/IFrameShow | Clean embeddable view (no layout chrome), used by the admin preview card |
/articles — Listing
- Search box — full-text search over title/content/excerpt (both languages), author, tags.
- Filters sidebar — Category (multi-select), Author, Year; Sort: Newest / Oldest / A–Z / Z–A.
- Result count + Pagination, empty state with reset button, sticky search bar with smooth scroll-back on filter change.
- Server-side list is capped at 100 most recent active articles; filtering/sorting/pagination happen client-side over that set. Older articles remain reachable by direct URL.
- Categories load lazily via
Inertia::defer(skeleton until ready).
/articles/{slug} — Detail
Only active articles are shown; anything else returns 404.
- Header: category badge, title, meta (author / published date / read time — hidden when empty), back link.
- Excerpt rendered as an HTML preview above the content.
- Body: rich-text content with a read-completion sentinel.
- Lucky Me interstitial — scroll to trigger a random other article.
- Sidebar: recommended articles (personalized), related articles by author.
- Mobile sections: recommendations + same-author articles.
- SEO: custom meta tags from the admin repeater are rendered as
<meta>tags (og:*keys useproperty, others usename); sensible fallbacks (description, og:title, og:description, og:type, og:image, article:author, article:section, article:published_time, og:url).
/articles/{slug}/iframe
Minimal article view used for embedding (admin preview, third-party embeds).
SEO Flow
The following flowchart shows how meta tags configured in the admin form are rendered on the public article page:
Predefined keys (from the SEO enum): description, keywords, author, robots, og:title, og:description, og:image, og:url, og:type, og:site_name, twitter:card, twitter:title, twitter:description, twitter:image — plus any custom name added via the + create option.
Always-rendered fallbacks on the detail page: og:title, og:type (article), og:url, og:image (if set), article:author, article:section, article:published_time, plus description / og:description derived from the excerpt when no custom SEO tag overrides them.
Analytics
Reading sessions are tracked client-side (start / scroll / completion) and pushed to the server.
Endpoint: POST /api/articles/track
Payload: article_id (required), fingerprint_id (required), session_id, read_duration_seconds, scroll_depth (0–100), completed, referrer_url.
Behavior:
updateOrCreateon (article, fingerprint, session) — one session per reader per article.- Duration and scroll depth only ever increase (monotonic).
- IP resolved to country/city/timezone via
ip-api.com(cached 24 h, best-effort, skipped for localhost). - Device type detected from user-agent (desktop / tablet / mobile).
- Completion is flagged when the reader scrolls past the article-end sentinel (IntersectionObserver).
Analytics flow — from reading session to dashboard
Recommendation Engine
RoundupService produces the "recommended articles" list (default 8) on the detail page.
Flow
Base score (cached in Redis 10 min, shared across visitors):
| Signal | Weight |
|---|---|
| Same category | 0.35 |
| Same school | 0.15 |
| Popularity (reads) | 0.15 |
| Recency (decay over 6 months) | 0.10 |
Personalization (applied per fingerprint on top of the cached base):
| Signal | Weight |
|---|---|
| Collaborative — readers of this article also read... | up to 0.25 |
| This user's category history | up to 0.15 |
Any article create / update / delete invalidates the cached roundups and analytics (ArticleObserver).
Database
| Table | Description |
|---|---|
articles | UUID PK; title/content/excerpt (localized JSON), slug (unique, route key), tags, category_id, school_id, author, is_active, published_at, image_asset_id, asset_alt_text, meta (SEO JSON) |
article_categories | UUID PK; localized name/description, slug, is_active |
article_analytics | UUID PK; article_id, fingerprint_id, session_id, ip_address, user_agent, referrer_url, started_at, last_active_at, read_duration_seconds, scroll_depth, completed, device_type, country, city, timezone |
Features
Admin — Article Management
- CRUD — create, edit, view, and delete articles; bulk delete from the list.
- Bilingual content — localized title, excerpt, and content (EN/ID) with per-language tabs.
- Auto-translate — copy the base-language content into the other language with one click.
- Rich-text editor — headings, subtitles, quotes, links, and a
/quick menu. - Insert media in content — upload and embed images directly inside the article body.
- Thumbnail — upload a featured image (with image editor) plus accessibility alt text, served via ImageKit.
- SEO meta tags — repeater of
{name, content}pairs with predefined + custom keys and unique-name validation; rendered as<meta>tags on the public page. - Scheduling — set a future publish date; the article stays unpublished until then.
- Tags — free-form tag input on the article.
- Categories — manage categories with localized names, or create one inline from the article form.
- Search & filters — full-text search (Meilisearch) plus status/school/category/date-range filters.
- Live device preview — view the public article inside desktop/tablet/mobile frames.
Admin — Analytics Dashboard
- Stats overview — total categories, published, and unpublished article counts.
- Reader Geography — treemap of reads by country.
- Popular Articles — reads, unique readers, total reading time, and completions per article.
Public — Articles
- Listing — browse
/articleswith search, category/author/year filters, sort (newest/oldest/A–Z/Z–A), pagination, and empty state. - Article detail — category badge, title, author/date/read-time meta, excerpt as an HTML preview, and the rich-text body.
- Recommendations — a personalized "recommended" list plus same-author related articles.
- Lucky Me — scroll interstitial that jumps to a random article.
- SEO — custom meta tags, Open Graph tags, and fallbacks rendered in
<head>. - Localization — switch EN ↔ ID on the fly without a reload.
- Embeddable view — clean
/articles/{slug}/iframeused for embedding and the admin preview.
Analytics (tracking)
- Reading sessions — track start, duration, scroll depth, and completion per reader.
- Geo-location — country/city/timezone resolved from IP (cached, best-effort).
- Device detection — desktop / tablet / mobile from the user-agent.
Key Files
| File | Purpose |
|---|---|
app/Filament/Clusters/Article/ArticleCluster.php | Cluster definition — slug news, navigation |
app/Filament/Clusters/Article/Resources/Articles/ArticleResource.php | Article resource — pages, localized data mutate/fill |
app/Filament/Clusters/Article/Resources/Articles/Schemas/ArticleForm.php | Article create/edit form |
app/Filament/Clusters/Article/Resources/Articles/Schemas/ArticleInfolist.php | Article view page + live preview card |
app/Filament/Clusters/Article/Resources/Articles/Tables/ArticlesTable.php | Article list — columns, filters, Meilisearch search |
app/Filament/Clusters/Article/Resources/ArticleCategories/ArticleCategoryResource.php | Category CRUD |
app/Filament/Clusters/Article/Pages/PopularArticlesPage.php | Analytics dashboard page |
app/Filament/Clusters/Article/Resources/Articles/Widgets/*.php | Stats, popular table, geography treemap widgets |
app/Filament/Schemas/Components/Cards/ArticlePreview.php | Live device preview component |
app/Http/Controllers/Landing/ArticleController.php | Public index / show / iframe |
app/Http/Controllers/Api/ArticleAnalyticController.php | Session tracking endpoint |
app/Http/Controllers/Api/ArticleContentController.php | Raw content fetch |
app/Http/Controllers/Api/ArticleLuckyController.php | Random-article endpoint |
app/Services/News/RoundupService.php | Recommendation scoring + personalization |
app/Services/News/AnalysisService.php | Analytics aggregations (cached) |
app/Observers/News/ArticleObserver.php | Cache invalidation on article changes |
app/Models/News/Article/Article.php | Article model |
app/Models/News/ArticleCategory/ArticleCategory.php | Category model |
app/Models/News/ArticleAnalytic/ArticleAnalytic.php | Analytics model |
resources/js/pages/landing/Articles/*.svelte | Public listing / detail / iframe pages |
resources/js/modules/landing/articles/ | Frontend stores, filters, analytics tracking, recommendation UI |
Glossary
| Term | Definition |
|---|---|
| Article | A news entry managed in the admin panel and shown on the public site |
| Slug | URL-friendly unique identifier of an article or category (e.g. registration-guide); used as the route key — an article is reachable at /articles/{slug} |
| Excerpt | Short summary of the article (localized, per language) shown as an HTML preview on the public detail page and used as the meta-description fallback |
| Content | The main body of the article, written in a rich-text editor (localized, per language) |
Published At (published_at) | The date/time the article becomes (or became) public; if set in the future the article stays hidden until that moment |
Is Active (is_active) | Flag controlling public visibility — only active articles appear on /articles and in search |
| Tags | Free-form keywords attached to an article; searchable via full-text search |
| Category | Grouping that organizes articles; categories are localized and can be active/inactive |
| Thumbnail | Featured image of the article (an Asset); shown in cards and used for og:image |
| Alt text | Accessibility description for the thumbnail image |
Meta (meta) | JSON column on the article holding the SEO repeater data (meta.seo — list of {name, content} pairs) |
| SEO meta tag | A single {name, content} entry configured in the admin repeater, rendered as <meta> on the public page (og:* keys use property) |
| Localization | Support for the same field in multiple languages (EN/ID) stored as JSON; the public page re-localizes without a reload |
Fingerprint (fingerprint_id) | Stable client-side identifier (cookie/generated) that identifies a reader across sessions without an account; used for analytics and recommendation personalization |
Session (session_id) | Identifier for one reading session of an article by a reader |
Read duration (read_duration_seconds) | Total seconds a reader spent on an article; only ever increases (monotonic) |
Scroll depth (scroll_depth) | How far (0–100%) the reader scrolled through the article |
Completed (completed) | Flag set when the reader scrolls past the article-end sentinel — used for completion metrics |
| Sentinel | A hidden element at the end of the article body observed via IntersectionObserver to detect completion |
| ArticleAnalytic | One row per article reading session, aggregating the metrics above |
| Lucky Me | Scroll-triggered interstitial on the detail page that navigates to a random other article |
| Recommended articles | A personalized list generated by RoundupService (category/school/popularity/recency + collaborative/history signals) |
| Reader Geography | Dashboard treemap of reads grouped by country, resolved from the reader's IP |
| Meilisearch | Full-text search engine backing the admin global search and the public search box |