π‘ Mostly live, with one honest caveat. The blog is real end-to-end β
authoring, AI drafting, scheduling, publishing, and a themed public renderer all
ship. The content planner (trend-aware idea queue) is live on free grounded
web research; its numeric keyword layer (DataForSEO) is βͺ dormant until
configured. The SEO Studio scores real page content but over-reports the
technical/site checks for folder-sites β sitemap, robots and structured data
are graded as present when the underlying generators are wired to the templated
main-site substrate, not the folder-site's hand-coded pages. Details in Β§5.
Audience: π οΈ Builder / operator Β· π§βπ» Engineer Β· π£ Marketer
See also: Marketing CC Β· Showcase Β·
the glossary (house terms: a unit of work is a Job; the person you build for is
a Client).
| Badge | Meaning |
|---|---|
| π’ | Live and wired end-to-end |
| π‘ | Live but partial / caveated |
| βͺ | Built but dormant (code exists, nothing drives it by default) |
| β | Not built |
Content marketing hangs off a Client's website, and a Client's website is now
built from two surfaces β the visual/templated website builder was removed:
/portal/website β the website manager (app/routers/website.py). It isedit_custom, app/routers/website.py:353), optionally filed into folders,save_folder_site, app/routers/website.py:221)./portal/blog β the blog dashboard (app/routers/blog_admin.py), aA subtlety worth internalising: every folder-site still renders through a
kind='site' LandingPage that owns the slug, subdomain, custom domain and public
shell. Since the templated builder is gone, that substrate is now created as a
minimal, unpublished row on demand (_ensure_site,
app/routers/website.py:46) β "just the plumbing," never edited by a builder UI.
That fact is the root of the SEO caveat in Β§5.
Gating: both routers require require_feature("marketing") and role
pm/office (app/routers/website.py:29, app/routers/blog_admin.py:33) β so a
Client's own users can self-serve on their instance.
A blog is per folder-site. Toggling it on flips blog_on and captures the
site's chrome (app/routers/blog_admin.py:124). Storage
(app/models/blog.py):
BlogPost β folder_id, slug (unique per folder), title, subtitle,excerpt, body_html (nh3-sanitized), cover_doc_id, status (draft|published),featured, published_at, author_kind (operator|auto), read_minutes, seo{title, description} JSON), ai_meta, and many-to-many categories.BlogCategory β per-folder, unique (folder_id, slug).BlogIdea β the content-planner queue (see Β§6).Folder-level blog settings live on CustomPageFolder
(app/models/custom_page.py:61): blog_on, blog_label, blog_intro,
blog_nav_html/blog_footer_html (captured chrome), and blog_auto /
blog_auto_freq / blog_auto_n for scheduling.
blog.create_post(... author_kind="operator")app/routers/blog_admin.py:158), then the editor at /portal/blog/post/{id}.edit, app/routers/blog_admin.py:258) edits title/subtitle/excerpt,body_html, categories (comma list, capped at 5), cover image (picked from theDocument images), and per-post SEO title/description. On saveapp/routers/blog_admin.py:282) the body is re-sanitized vialanding.sanitize_html(cap=60000) and read_minutes recomputed.feature,app/routers/blog_admin.py:324); the renderer gives the featured post a wideblog_ai) π’blog_ai.generate (app/services/blog_ai.py:135) turns a brief into a draft
(it never auto-publishes). The brief = topic/angle + optional referenced
Job(s) + target city/service + tone + length (short/medium/long,
app/services/blog_ai.py:22). Key behaviours:
anonymized_jobs,app/services/blog_ai.py:39; city is derived from the segment after the first_SYSTEM, app/services/blog_ai.py:79).ai_assistant._build_contextjob_photo_ids, app/services/blog_ai.py:64);app/routers/blog_admin.py:187).title, subtitle, excerpt, body_html (only<h2>/<h3>/<p>/<ul>/<li>/<blockquote>/<strong>/<em> β no scripts/styles/classes),categories, plus seo_title/seo_description. Runs at temperature=0.7,max_tokens=2200, json_mode=True on the shared LLM clientapp/integrations/ai/deepseek.py).The route (generate, app/routers/blog_admin.py:170) writes the draft and records
the full brief in ai_meta (with "anonymized": True), then drops the operator
into the editor. On AIError it redirects with ?err=ai.
set_status (app/services/blog.py:117) flipsstatus and stamps published_at on first publish (publish,app/routers/blog_admin.py:312). No draft/scheduled-future date field; publish isblog_auto +blog_auto_freq (weekly/biweekly/monthly) + blog_auto_n (1β10). Therun_due_blog_drafts β blog_ai.run_dueapp/services/scheduler.py:187, app/services/blog_ai.py:174). It generatesauthor_kind="auto" post), and pulls the top planner idea when oneapp/services/blog.py renders each article as a standalone, self-contained HTML
document that looks native to the folder-site β deliberately decoupled from the
site-builder path (app/services/blog.py:11):
style.css and wears its captured<nav>/<footer> (capture_chrome, app/services/blog.py:142), then layers a.gbb "prose" theme built on the site's CSS variables_article_css, app/services/blog.py:184).BLOG_NAV_JS, app/services/blog.py:166)._live_scripts, app/services/blog.py:234) β see GoBuild Live.<title>/meta description/canonical, Open Graph +<link>, and BlogPosting JSON-LD (render_post,app/services/blog.py:350). The index and each post also get a CTA back to thePublic routes (app/routers/landing_portal.py): /p/{slug}/blog (index, with
?cat= filter and ?preview= to see drafts), /p/{slug}/blog/{post_slug}, and
/p/{slug}/blog/feed.xml (30-item RSS, feed_xml, app/services/blog.py:368).
When a folder-site is served on its own domain, the host middleware rewrites
/blogβ¦ β /p/{slug}/blogβ¦ (app/main.py:182), so posts live at the site's real
domain.
The through-line: folder-sites are the unit of publishing, and both custom
pages and the blog attach to them.
subdomain set β i.e. foldershub, app/routers/blog_admin.py:87)./portal/website) are the rest of the site around the blog:cp.seo), nav labels,gobuild-page skill for scaffolding a bespoke page.The SEO Studio is the panel on /portal/website, backed by
app/services/seo_analysis.py and served as JSON by folder_seo
(app/routers/website.py:268); dashboard badges come from _folder_seo_badges
(app/routers/website.py:157).
What's genuinely real (per-page content scoring). analyze_page
(app/services/seo_analysis.py:83) reads each custom page's actual HTML and scores
title length, meta description, single-H1, subheading structure, content depth
(word count), image alt coverage, the best OG image (largest self-hosted photo),
local-SEO geo signals, and NAP/phone presence. These reflect the real shipped
output and are trustworthy. It also produces a Google-style snippet preview and a
prioritised recommendations list (analyze_site, app/services/seo_analysis.py:152).
The caveat β the technical/site checks over-report for folder-sites. Several
checks pass purely on has_domain (subdomain or custom domain set), and their
"detail" strings promise artifacts that the folder-site does not actually emit:
| Check | Studio says | Reality for a folder-site |
|---|---|---|
XML sitemap (seo_analysis.py:169) |
"Auto-generated at /sitemap.xml" | /sitemap.xml is generated from the kind='site' templated substrate (sitemap, landing_portal.py:566) β it lists builder pages + local combos, not the folder's custom pages or blog posts. That substrate is auto-created unpublished (_ensure_site), so the route can even 404. |
robots.txt (seo_analysis.py:171) |
"Allows crawlers + links the sitemap" | Same substrate dependency (robots, landing_portal.py:556); returns Disallow: / when the main page is unpublished. |
Structured data (seo_analysis.py:124) |
"Schema.org LocalBusiness JSON-LD" | LocalBusiness JSON-LD is injected only by the builder page renderer (landing_portal.py:184). A folder-site's hand-coded custom pages emit no LocalBusiness schema. (Blog posts do emit BlogPosting JSON-LD β a different type, from a different renderer.) |
Canonical URL (seo_analysis.py:122) |
passes on has_domain |
Custom pages don't necessarily emit a <link rel=canonical> unless the hand-coded HTML includes one. |
Net: the Studio's overall score blends 45% site-technical + 55% page-content
(analyze_site, app/services/seo_analysis.py:181). The page-content 55% is honest;
the site-technical 45% is optimistic for folder-sites. Treat the letter grade as a
content-quality signal, not proof the site is crawlable. Fixing this means
either wiring real per-folder sitemap/robots/schema generation off the folder's
custom pages, or downgrading those checks to warn until they're actually shipped.
app/services/content_planner.py turns a Client's industry Γ service-area +
live trend research into ranked, trend-aware BlogIdea rows.
_brand_inputs, app/services/content_planner.py:29) β trades +kind='site'live_ai_kb) as brand-specificsuggest_ideas, app/services/content_planner.py:112) β callstrends.research(...) (free grounded web trend research, app/services/trends.py:45)trends.keyword_signals(...). The LLM (temperature=0.75, JSON mode) clustersn ideas, each with an angle, target_keyword, service, city,trend direction, rationale ("why now"), and a 0β100 score.keyword_volume/competition/trend aredataforseo_configured (trends.paid_configured,app/services/trends.py:39). Until then keyword_signals returns [] and theapp/routers/blog_admin.py:119).suggested β used (drafted) or dismissedapp/routers/blog_admin.py:215, :246); re-suggestion dedupes against used/app/services/content_planner.py:139). Turning an idea into ablog_ai.generate with the idea's angle/keyword/rationale as the prompt.refresh_blog_ideas β blog_ai.refresh_ideas_due,app/services/scheduler.py:181, app/services/blog_ai.py:254), so operators| Capability | Status | Notes |
|---|---|---|
| Blog authoring + editor | π’ | Per folder-site; sanitized HTML |
AI drafting (blog_ai) |
π’ | Anonymizes Jobs; draft-only |
| Publish / unpublish / feature | π’ | Immediate; no future-date scheduling |
| Scheduled auto-drafts | π‘ | Opt-in per folder; drafts only |
| Public themed renderer + RSS + BlogPosting JSON-LD | π’ | Inherits site chrome/CSS |
| Content planner (grounded research) | π’ | Free web-trend tier |
| Content planner (DataForSEO numbers) | βͺ | Dormant until configured |
| SEO Studio β page-content scoring | π’ | Reflects real HTML |
| SEO Studio β site/technical checks | π‘ | Over-reports for folder-sites (Β§5) |
| Visual/templated website builder | β | Removed; custom-pages + folder-sites only |