---
title: "Homepage for publishers | Snowplow Signals"
description: "Snowplow Signals tells your homepage and CMS how each article is doing with the readers on the site now, so the story that works moves up while it works."
source: https://signals.snowplow.io/solutions/media/homepage
---

[Media and publishing](https://signals.snowplow.io/solutions/media) / Homepage 

# A homepage that knows which stories are working. 

Signals tells your homepage and CMS how each article is doing with the readers on the site now, so placement can follow the story that works instead of waiting for the next check.

[Start free](https://signals.snowplow.io/start) [Talk to us](https://signals.snowplow.io/start?intent=talk) 

More clicks from the homepage

## Get more clicks from the homepage by moving the story that works while it works. 

Homepage clicks move when placement can follow what readers are doing now rather than the plan from the morning meeting. Signals gives your homepage and CMS how each article, each section and each reader is doing over the last few minutes, so the story pulling readers in gets the slot, the one being skipped gives it up, and a reader who has finished a story is shown one they have not.

Four ways publishers use it

01 A story taking off below the fold Twelfth in its section, a hundred readers in five minutes. Move it up. 02 A lead story that is seen and not opened Shown to everyone for ten minutes, opened by almost no one. Swap the slot. 03 A section drawing readers this hour A story broke in business and readers are following it. Give the block room. 04 A reader who has finished the top stories Read the top three on Tuesday, closed the fourth. Show them something new. 

[Read the docs →](https://docs.snowplow.io/docs/signals/introduction/) 

### Promote a story while it is pulling readers in

A story the morning meeting put twelfth in its section has been drawing readers for the last few minutes, most of them arriving from social. The homepage still shows the order from the meeting, and the next look at the dashboard comes after the surge has passed. The slot this story needs is the one above it, and it needs it now.

What Signals knows

`views_5m` 

Page views of this article in the last five minutes

`sessions_5m` 

Separate visits that opened it in the last five minutes

`arriving_from_5m` 

Where most of those readers came from

What your homepage does

Moves an article up within its section block when its separate visits in the last five minutes pass the threshold the desk set for that block, and leaves the lead slot to editorial.

Define it · PythonUse it · TypeScript

article.tsx · 11 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import (
    Attribute, AttributeKey, AtomicProperty, EntityProperty, Event, StreamAttributeGroup,
)

page_view = Event(name="page_view", vendor="com.snowplowanalytics.snowplow", version="1-0-0")
article = AttributeKey(
    name="article",
    property=EntityProperty(vendor="com.example", name="article", major_version=1, path="id"),
)

views_5m = Attribute(
    name="views_5m",
    type="int32",
    events=[page_view],
    aggregation="counter",
    period=timedelta(minutes=5),
    default_value=0,
)

sessions_5m = Attribute(
    name="sessions_5m",
    type="int32",
    events=[page_view],
    aggregation="approx_count_distinct",
    property=AtomicProperty(name="domain_sessionid"),
    period=timedelta(minutes=5),
)

arriving_from_5m = Attribute(
    name="arriving_from_5m",
    type="string",
    events=[page_view],
    aggregation="most_frequent",
    property=AtomicProperty(name="refr_medium"),
    period=timedelta(minutes=5),
)

# Keyed on the article, not the reader, so the homepage asks about a story.
homepage_rising = StreamAttributeGroup(
    name="homepage_rising",
    version=1,
    attribute_key=article,
    owner="you@example.com",
    attributes=[views_5m, sessions_5m, arriving_from_5m],
)
```

```
// In the code that renders the homepage, for each story in a section block
const story = await signals.getServiceAttributes({
  name: "homepage_rising",
  attribute_key: "article",
  identifier: articleId,
});
// { views_5m: 112, sessions_5m: 104, arriving_from_5m: "social" }

if (story.sessions_5m >= block.promoteAt) {
  homepage.promote({ article: articleId, within: block.section });
}
```

### Swap a lead story that readers see and skip

The lead slot is shown to every reader who lands on the homepage, and for the last ten minutes very few of them have opened it. The story may be right and the headline wrong, or the moment may have passed. The plan from the meeting holds it there until someone looks, and by then the slot has been spent on readers who have gone.

What Signals knows

`teaser_shown_10m` 

Times its teaser was shown on the homepage in the last ten minutes

`teaser_clicks_10m` 

Times that teaser was opened in the last ten minutes

What your homepage does

Compares opens to showings over the last ten minutes. When the share falls under the floor set for the lead slot, it swaps in the next story from the plan, or sends the desk a nudge in the CMS so an editor can try a different headline.

Define it · PythonUse it · TypeScript

article.tsx · 14 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, AttributeKey, EntityProperty, Event, StreamAttributeGroup

teaser_shown = Event(name="teaser_shown", vendor="com.example", version="1-0-0")
teaser_clicked = Event(name="teaser_clicked", vendor="com.example", version="1-0-0")
article = AttributeKey(
    name="article",
    property=EntityProperty(vendor="com.example", name="article", major_version=1, path="id"),
)

teaser_shown_10m = Attribute(
    name="teaser_shown_10m",
    type="int32",
    events=[teaser_shown],
    aggregation="counter",
    period=timedelta(minutes=10),
    default_value=0,
)

teaser_clicks_10m = Attribute(
    name="teaser_clicks_10m",
    type="int32",
    events=[teaser_clicked],
    aggregation="counter",
    period=timedelta(minutes=10),
    default_value=0,
)

homepage_lead = StreamAttributeGroup(
    name="homepage_lead",
    version=1,
    attribute_key=article,
    owner="you@example.com",
    attributes=[teaser_shown_10m, teaser_clicks_10m],
)
```

```
// In the code that renders the homepage, for the story in the lead slot
const lead = await signals.getServiceAttributes({
  name: "homepage_lead",
  attribute_key: "article",
  identifier: articleId,
});
// { teaser_shown_10m: 2300, teaser_clicks_10m: 19 }

const share = lead.teaser_clicks_10m / lead.teaser_shown_10m;

if (lead.teaser_shown_10m >= leadSlot.minShown && share < leadSlot.floor) {
  homepage.swap({ slot: "lead", article: plan.next(articleId) });
  cms.nudge({ article: articleId, note: "Lead is being seen and skipped" });
}
```

### Give a section more room while readers are heading there

A story broke in business an hour ago and readers have been heading to that section since, from the homepage and straight in from search. The homepage grid holds business to the four slots the template allows on a quiet day. The grid gets rebalanced when someone gets to it, which lags behind the hour, so the busiest section on the site has the same room as the quietest.

What Signals knows

`views_1h` 

Page views across this section in the last hour

`sessions_1h` 

Separate visits that read something in it in the last hour

`top_story_1h` 

The article in it read most in the last hour

What your homepage does

Gives a section block more slots when its separate visits in the last hour pass the level the desk set for a busy section, and puts the article drawing most of them at the top of the block.

Define it · PythonUse it · TypeScript

article.tsx · 12 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import (
    Attribute, AttributeKey, AtomicProperty, EntityProperty, Event, StreamAttributeGroup,
)

page_view = Event(name="page_view", vendor="com.snowplowanalytics.snowplow", version="1-0-0")
section = AttributeKey(
    name="section",
    property=EntityProperty(vendor="com.example", name="article", major_version=1, path="section"),
)

views_1h = Attribute(
    name="views_1h",
    type="int32",
    events=[page_view],
    aggregation="counter",
    period=timedelta(hours=1),
    default_value=0,
)

sessions_1h = Attribute(
    name="sessions_1h",
    type="int32",
    events=[page_view],
    aggregation="approx_count_distinct",
    property=AtomicProperty(name="domain_sessionid"),
    period=timedelta(hours=1),
)

top_story_1h = Attribute(
    name="top_story_1h",
    type="string",
    events=[page_view],
    aggregation="most_frequent",
    property=EntityProperty(vendor="com.example", name="article", major_version=1, path="id"),
    period=timedelta(hours=1),
)

# Keyed on the section path of the same article entity, so one event feeds both keys.
homepage_section = StreamAttributeGroup(
    name="homepage_section",
    version=1,
    attribute_key=section,
    owner="you@example.com",
    attributes=[views_1h, sessions_1h, top_story_1h],
)
```

```
// In the code that lays out the homepage grid, for each section block
const section = await signals.getServiceAttributes({
  name: "homepage_section",
  attribute_key: "section",
  identifier: sectionId,
});
// { views_1h: 8400, sessions_1h: 6100, top_story_1h: "a4471" }

if (section.sessions_1h >= grid.busyAt) {
  homepage.resize({ block: sectionId, slots: grid.busySlots });
  homepage.promote({ article: section.top_story_1h, within: sectionId });
}
```

### Keep the stories a reader has finished out of their first screen

A reader comes back for the third time this week and the first screen is the same three stories they read on Tuesday, plus the one they closed after a paragraph. The homepage is laid out for the newsroom's idea of a reader, and that layout holds for everyone until the plan changes. For this reader the above-the-fold slots go to stories they are done with, and the ones they have not seen sit below.

What Signals knows

`articles_read_7d` 

The articles this reader read to the end in the last seven days

`articles_dismissed_7d` 

The teasers this reader closed in the last seven days

What your homepage does

Skips any story on this reader's read or dismissed list when filling their above-the-fold slots, and pulls the next story in the plan up in its place. The plan itself is unchanged, and so is the page for everyone else.

Define it · PythonUse it · TypeScript

article.tsx · 12 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import (
    Attribute, Criteria, Criterion, EntityProperty, Event, EventProperty, StreamAttributeGroup, domain_userid,
)

article_read = Event(name="article_read", vendor="com.example", version="1-0-0")
teaser_dismissed = Event(name="teaser_dismissed", vendor="com.example", version="1-0-0")
article_id = EntityProperty(vendor="com.example", name="article", major_version=1, path="id")

articles_read_7d = Attribute(
    name="articles_read_7d",
    type="string_list",
    events=[article_read],
    aggregation="unique_list",
    property=article_id,
    criteria=Criteria(all=[
        Criterion.gte(EventProperty(vendor="com.example", name="article_read", major_version=1, path="percent_read"), 80),
    ]),
    period=timedelta(days=7),
)

articles_dismissed_7d = Attribute(
    name="articles_dismissed_7d",
    type="string_list",
    events=[teaser_dismissed],
    aggregation="unique_list",
    property=article_id,
    period=timedelta(days=7),
)

homepage_seen = StreamAttributeGroup(
    name="homepage_seen",
    version=1,
    attribute_key=domain_userid,
    owner="you@example.com",
    attributes=[articles_read_7d, articles_dismissed_7d],
)
```

```
// In the code that renders the homepage for this reader
const reader = await signals.getServiceAttributes({
  name: "homepage_seen",
  attribute_key: "domain_userid",
  identifier: domainUserId,
});
// { articles_read_7d: ["a4412", "a4418", "a4420"], articles_dismissed_7d: ["a4425"] }

const done = new Set([...reader.articles_read_7d, ...reader.articles_dismissed_7d]);
const aboveFold = plan.filter((story) => !done.has(story.id)).slice(0, plan.aboveFoldSlots);

homepage.render({ aboveFold, below: plan.rest(aboveFold) });
```

## Try it on your own site. 

Add the SDK, define one thing you want to know about a visitor, and watch it update while you click around. The free tier runs on the real engine: no card, no sales call.

[Start free](https://signals.snowplow.io/start) [Talk to an engineer](https://signals.snowplow.io/start?intent=engineer)
