Plan: Data-Driven Home Page (home-v2.html → /)

Goal

Turn htmltemplate/home-v2.html (the "Documentation hub" landing page) into the application's real home page served at /. Every piece of content becomes data-driven: a new HomePage config model + linked models drive the hero, featured cards, quick paths, quick-install, and "Recently updated" sections, and the site list is rendered from the existing DocumentationSite records (extended with category / icon / featured / order). A new cross-site search endpoint backs the command palette. Branding is configurable (default "Enezadocs").

The current root URL (config/urls.py:9) redirects to documentation:site_index; it will instead render the new home page.

Locked decisions

Decision Choice
Integration home-v2.html → new Django template documentation/home.html rendered at / by a new HomeView.
Data model HomePage singleton + QuickPath + FeaturedSite + HomeUpdate; extend DocumentationSite with category, icon, featured, order.
Recently updated Curated HomeUpdate model (admin-edited), newest first.
Search New cross-site endpoint GET /search/?q= (root-level), wired to the command palette.
Branding HomePage.brand_name (default "Enezadocs"); logo letter = first char of brand_name.
Site card fields icon/name/category/description derived from DocumentationSite; "guides" count from DocumentationSearchDocument per source; "updated" from site.updated_at via timesince.
Featured card links Derived: first up-to-4 documents of the featured site's navigation tree.
Quick install HomePage fields: title, description, code (Text), bullets (newline-separated Text).
Version dropdown Dropped (no DocumentationVersion model); "Latest release" stat reads HomePage.latest_release.
Header/footer nav + legal Simplified static links (Documentation → /docs/, Changelog → #updates, Sign in/up via allauth, Terms/Privacy placeholders). Not modeled.
Home layout Standalone template (does not extend documentation/base.html, which is the 3-column docs layout).
Styling/JS Extract home-v2 <style> → static/documentation/css/home.css; new home.js; reuse theme.js (IDs already match).

Files to change / create

  • apps/documentation/models.py — extend DocumentationSite; add HomePage, QuickPath, FeaturedSite, HomeUpdate.
  • apps/documentation/admin.py — extend DocumentationSiteAdmin; register HomePage (with inlines) + new models.
  • apps/documentation/views.py — add HomeView, SearchAllView.
  • config/urls.py — root "" → HomeView; add search/ → SearchAllView.
  • apps/documentation/templates/documentation/home.html — new (from home-v2.html).
  • apps/documentation/static/documentation/css/home.css — new (home-v2 <style>).
  • apps/documentation/static/documentation/js/home.js — new (palette + search fetch + category filter + copy).
  • Migration(s) via makemigrations/migrate (run by implementer).

Ordered tasks

1. Extend DocumentationSite (models.py)

Add fields following the repo convention (one field per line, verbose_name on every field, closing ) on its own line aligned with the field start):

  • Icon TextChoices inner class: layers, code, terminal, shield, database, globe, card, box (values match home-v2 ICONS keys).
  • category = CharField(max_length=100, blank=True, verbose_name="category")
  • icon = CharField(max_length=20, choices=Icon.choices, default="layers", verbose_name="icon")
  • featured = BooleanField(default=False, verbose_name="featured")
  • order = PositiveIntegerField(default=0, verbose_name="order")

Add ordering = ["order", "name"] to Meta (keep existing verbose_name/verbose_name_plural).

2. Add HomePage singleton (models.py)

Fields (each on its own line + verbose_name): - brand_name = CharField(max_length=100, default="Enezadocs", verbose_name="brand name") - hero_eyebrow = CharField(max_length=100, default="Documentation hub", verbose_name="hero eyebrow") - hero_title = CharField(max_length=300, default="Every Eneza product, documented in one place.", verbose_name="hero title") - hero_subtitle = TextField(blank=True, verbose_name="hero subtitle") - cta_title = CharField(max_length=200, default="Can't find what you need?", verbose_name="cta title") - cta_subtitle = TextField(blank=True, verbose_name="cta subtitle") - footer_tagline = TextField(blank=True, verbose_name="footer tagline") - latest_release = CharField(max_length=50, default="v1.0", verbose_name="latest release") - quick_install_title = CharField(max_length=200, blank=True, verbose_name="quick install title") - quick_install_description = TextField(blank=True, verbose_name="quick install description") - quick_install_code = TextField(blank=True, verbose_name="quick install code") - quick_install_bullets = TextField(blank=True, verbose_name="quick install bullets", help_text="One bullet per line.") - created_at / updated_at (auto timestamps). - class Meta: verbose_name = "home page"; verbose_name_plural = "home page". - Classmethod load() → HomePage.objects.get_or_create(pk=1, defaults={...}) returns the singleton.

3. Add QuickPath, FeaturedSite, HomeUpdate (models.py)

  • QuickPath: home = ForeignKey(HomePage, on_delete=CASCADE, related_name="quick_paths", verbose_name="home page"), label = CharField(max_length=200, verbose_name="label"), query = CharField(max_length=200, verbose_name="query"), order = PositiveIntegerField(default=0, verbose_name="order"); Meta: ordering = ["order", "id"].
  • FeaturedSite: home FK (related_name "featured_sites"), site = ForeignKey(DocumentationSite, on_delete=CASCADE, related_name="featured_entries", verbose_name="site"), order, optional title = CharField(blank=True) / subtitle = TextField(blank=True) overrides; Meta: ordering = ["order", "id"].
  • HomeUpdate: home FK (related_name "updates"), site = ForeignKey(DocumentationSite, on_delete=SET_NULL, null=True, blank=True, related_name="updates", verbose_name="site"), text = CharField(max_length=500, verbose_name="text"), published_at = DateTimeField(default=timezone.now, verbose_name="published at"); Meta: ordering = ["-published_at"].

4. Migrations

manage.py makemigrations documentation then manage.py migrate (implementer runs; user validates).

5. Admin (admin.py)

  • DocumentationSiteAdmin: add category, icon, featured, order to list_display; list_filter += ("category", "featured"); list_editable = ("order", "featured", "category").
  • HomePageAdmin with has_add_permission returning False once a HomePage exists (singleton); inlines QuickPathInline, FeaturedSiteInline, HomeUpdateInline (all extra = 1). Register QuickPath, FeaturedSite, HomeUpdate (inlines suffice; standalone registration optional).

6. Views (views.py)

HomeView(View): - home = HomePage.load(). - sites = list(DocumentationSite.objects.filter(enabled=True)) (respect ordering). - Build context: home, sites, categories (distinct non-empty category with counts), featured (each FeaturedSite + derived first-up-to-4 docs via DocumentationService(site, authenticated=request.user.is_authenticated).tree() → flatten() → document nodes), quick_paths, updates, and stats: - stat_sites = len(sites) - stat_articles = DocumentationSearchDocument.objects.filter(source__sites__in=sites).count() (or per-source sum) - stat_categories = len(categories) - stat_latest = home.latest_release - Per-site guides count = DocumentationSearchDocument.objects.filter(source=site.source).count(). - Render documentation/home.html.

SearchAllView(View) (JSON): - q = request.GET.get("q", "").strip(). - For each enabled site: run DocumentationService(site, authenticated=request.user.is_authenticated).search(q) (already scopes private docs) → tag each with type="article", site=site.name, slug=site.slug, href=f"/docs/{site.slug}/{path}/". - Add site-name matches: type="site", href=f"/docs/{site.slug}/" when q matches name/description/category. - Return {"results": [...]} (dedupe/limit ~20).

7. URLs (config/urls.py)

  • Import HomeView, SearchAllView from documentation.views; drop RedirectView.
  • path("docs/", include("documentation.urls")) (unchanged).
  • path("search/", SearchAllView.as_view(), name="search").
  • path("", HomeView.as_view(), name="home") (replaces the redirect).

8. Home template (home.html)

Standalone document mirroring home-v2.html structure, server-rendered: - <head>: inline no-FOUC theme script + tailwind config (copy from prototype), <link href="{% static 'documentation/css/home.css' %}">, {% block title %}{{ home.brand_name }}{% endblock %}. - Header: logo letter {{ home.brand_name|slice:":1" }}, "Documentation" → /docs/, "Changelog" → #updates, search button (opens palette), theme toggle (IDs themeBtn/themeSun/themeMoon), auth: if user.is_authenticated show logout form ({% url 'account_logout' %}) else sign-in link ({% url 'account_login' %}). - Hero: eyebrow/title/subtitle from home; search button (palette); quick-path chips {% for p in quick_paths %} with data-q="{{ p.query }}"; stats grid. - Featured: "Start here" — render featured cards (mark icon via a template {% include %} or an icon_svg filter; derive up-to-4 doc links + "Open documentation" → /docs/<slug>/); "Quick install" card from home.quick_install_* (code in <pre>, bullets split by newline). - Sites list: category filter chips (server-rendered with counts, data-cat); site cards {% for site in sites %} with data-cat, icon svg, name, category tag, description, guides guides, updated timesince, arrow → /docs/<slug>/. Include the empty-state block (used by JS filter). - Recently updated: {% for u in updates %} (site name, icon, text, published_at|timesince). - CTA + footer (brand, tagline, site links derived from sites, Account links, Terms/Privacy #). - Command palette markup (unchanged IDs: palette, palInput, palList, palCount) + toast. - Include {% static 'documentation/js/theme.js' %} and {% static 'documentation/js/home.js' %} at end of body.

9. CSS (home.css)

Create static/documentation/css/home.css containing the <style> block from home-v2.html (lines 19–131): theme variables, base, buttons, chips, search, site-card, tag, palette, toast, reveal. Ensure the --warn variable and scroll-col styles are retained.

10. JS (home.js)

Adapt home-v2's script, removing SITES/ARTICLES/UPDATES arrays and the esc/iconSvg client rendering: - Category filter: read #siteList .site-card[data-cat], toggle hidden by active chip, update #resultCount, show/hide #emptyState; wire chip clicks and #clearFilters. - Command palette: open/close (Ctrl-K, Esc, overlay, buttons palOpen/palOpenHero/palOpenSites/palOpenMobile); on input, debounced fetch('/search/?q='+encodeURIComponent(q)) → render grouped results ("Sites" / "Articles") with highlight; arrow/enter keyboard nav → navigate to result.href; "Quick paths" chips open palette seeded with data-q. - Copy button for the quick-install code block. - Keep the a[href="#"] neutralizer.

Validation (user-run — agent does not test)

  1. manage.py migrate (new fields + models) then manage.py runserver.
  2. In admin: edit HomePage (brand, hero, CTA, quick install, latest release); set a couple of DocumentationSite records with category, icon, featured, order; add QuickPath, FeaturedSite, HomeUpdate rows.
  3. Open / — verify hero, stats (sites/articles/categories/latest release), featured cards (with derived doc links), sites list (cards + category filter chips + counts + timesince), quick-install code/copy, recently updated, CTA, footer.
  4. Verify header auth (Sign in ↔ Sign out) and that /accounts/login/, /accounts/signup/ links resolve.
  5. Open the palette (Ctrl-K or search buttons), type a query, confirm results from the /search/?q= endpoint (sites + articles) render and navigate to /docs/<slug>/....
  6. Verify theme toggle + no-FOUC (dark/light) and mobile drawer behavior.
  7. Confirm /docs/ (site index listing) still works and old / redirect no longer fires.

Risks / notes

  • Model field convention (repo rule): every field on its own line with verbose_name; multi-line declarations close ) on its own line aligned with field start. Apply to all new fields.
  • Singleton HomePage: use pk=1 + get_or_create in load(); block extra adds in admin so only one row exists.
  • Source→site mapping for "guides"/articles: a DocumentationSource can back multiple sites; count search docs per source (acceptable for the card badge).
  • Cross-site search privacy: reuse DocumentationService.search(q), which already limits anonymous users to public docs, so the home palette does not leak private content.
  • Featured doc links trigger provider tree reads for each featured site; local sources are cheap and GitHub trees are cached (docs:site:<id>:tree), so keep featured list small.
  • Template convention: home is standalone (not base.html); it must include the no-FOUC script, tailwind config, home.css, theme.js, and home.js itself.
  • icon mapping: keep DocumentationSite.Icon choices exactly matching the SVG keys in home.css/template icon map (layers, code, terminal, shield, database, globe, card, box).

Out of scope

  • DocumentationVersion / version dropdown restoration.
  • Modeling header/footer nav links and legal pages (Terms/Privacy) as data.
  • Per-site reading-time ("X min") on featured links (dropped).
  • The non-functional inline text search box in the sites section (prototype referenced a missing #qHero); category chips + cross-site palette cover search.
  • Frontend build/Tailwind compilation (CDN retained, matching the existing app).
  • Testing by the agent (user runs the validation steps above).