Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/content/CHARTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ These exist so the template stays a **template**, not a half-finished product:
| Total `.ts` LOC under `src/` | ≤ 6,000 | Readable in a half-day sitting |
| Locales | 2 (`en`, `zh-CN`) | Proves multi-locale without becoming a translation project |
| Dashboards | 3 | Today / Calendar / ROI — each shows a distinct widget class |
| Flows | ≤ 4 | Event-driven only (no cron in spec v6); time-based work ships as record actions |
| Flows | ≤ 5 | Event-driven only (no cron in spec v6); time-based work ships as record actions. Cap raised 4→5 (2026-06): the package already ships five distinct event-driven flows — signal→topic promotion, CTA default, publish approval, publication rollup, and lifecycle notifications (the latter was renamed from `stamp_lifecycle_timestamps` once timestamp-stamping moved into the piece hook). |
| State machines | 2 | piece + signal |
| Approval processes | 1 | publish gate |
| Sharing rules | 1 | topic-scoped |
Expand Down
17 changes: 10 additions & 7 deletions packages/content/src/flows/publication_rollup.flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ type Flow = Automation.Flow;
* the parent `content_piece` so its dashboard/widget filters don't have
* to join.
*
* Triggered on metric insert/update. The flow reads all metric rows for
* the snapshot's publication, sums them, then writes back denormalised
* totals on both the publication and (via a second step) the piece.
* Triggered when a metric snapshot is **created** — metric rows are
* append-only weekly snapshots (see `record_metric_snapshot.action.ts`,
* which inserts a new row each period), so the rollup must fire on insert.
* (The earlier `record-after-update` trigger never fired on the real
* recording path, so publication/piece totals only matched in seed data.)
* The flow reads all metric rows for the snapshot's publication, sums them,
* then writes back denormalised totals on both the publication and (via a
* second step) the piece.
*
* Why denormalised: the analytics service in spec 5.2 doesn't allow
* filtered SUM(child.*) from a parent list view. Storing totals on the
Expand All @@ -22,7 +27,7 @@ export const PublicationRollupFlow: Flow = {
name: 'content_publication_rollup',
label: 'Roll Up Metrics to Publication & Piece',
description:
'On metric snapshot insert/update, refresh the publication.total_* and piece.total_* denormalised fields.',
'On metric snapshot create, refresh the publication.total_* and piece.total_* denormalised fields.',
type: 'record_change',

variables: [{ name: 'metricId', type: 'text', isInput: true, isOutput: false }],
Expand All @@ -34,9 +39,7 @@ export const PublicationRollupFlow: Flow = {
label: 'Start',
config: {
objectName: 'content_metric',
triggerType: 'record-after-update',
condition:
'previous.views != record.views || previous.clicks != record.clicks || previous.signups != record.signups || previous.revenue != record.revenue',
triggerType: 'record-after-create',
},
},
{
Expand Down
21 changes: 21 additions & 0 deletions packages/content/src/objects/content_piece.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,27 @@ export const Piece = ObjectSchema.create({
group: 'body',
}),

// SEO — the pre-publish checklist these teams actually track.
seo_title: Field.text({
label: 'SEO Title',
maxLength: 70,
group: 'body',
description: 'Title tag (≤ 60–70 chars). Falls back to the piece title if blank.',
}),
meta_description: Field.text({
label: 'Meta Description',
maxLength: 160,
group: 'body',
description: 'Search-result snippet (≤ 160 chars).',
}),
primary_keyword: Field.text({
label: 'Primary Keyword',
maxLength: 120,
searchable: true,
group: 'body',
description: 'The target query this piece is optimised to rank for.',
}),

// Lifecycle stamps
submitted_at: Field.datetime({ label: 'Submitted At', readonly: true, group: 'planning' }),
approved_at: Field.datetime({ label: 'Approved At', readonly: true, group: 'planning' }),
Expand Down
3 changes: 3 additions & 0 deletions packages/content/src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ export const en: TranslationData = {
summary: { label: 'Summary' },
body_outline: { label: 'Outline' },
body_draft: { label: 'Draft' },
seo_title: { label: 'SEO Title' },
meta_description: { label: 'Meta Description' },
primary_keyword: { label: 'Primary Keyword' },
submitted_at: { label: 'Submitted At' },
approved_at: { label: 'Approved At' },
published_at: { label: 'Published At' },
Expand Down
3 changes: 3 additions & 0 deletions packages/content/src/translations/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ export const zhCN: TranslationData = {
summary: { label: '摘要' },
body_outline: { label: '大纲' },
body_draft: { label: '草稿' },
seo_title: { label: 'SEO 标题' },
meta_description: { label: 'Meta 描述' },
primary_keyword: { label: '主关键词' },
submitted_at: { label: '提交时间' },
approved_at: { label: '通过时间' },
published_at: { label: '发布时间' },
Expand Down
Loading