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
54 changes: 54 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// @ts-check
import fs from 'node:fs';
import { execSync } from 'node:child_process';
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
import starlight from '@astrojs/starlight';
import starlightSidebarTopics from 'starlight-sidebar-topics';
import starlightLinksValidator from 'starlight-links-validator';
Expand Down Expand Up @@ -39,6 +41,50 @@ const redirects = {
'/websockets/topics/chatbot/timeout': '/websockets/topics/chatbot-timeout',
};

/** Most recent git commit date per content file, for sitemap <lastmod>.
One `git log` pass; the first time a file appears is its newest commit. */
function buildLastmodMap() {
const map = new Map();
const out = execSync('git log --format=%x00%cI --name-only -- src/content', {
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
});
let current = null;
for (const raw of out.split('\n')) {
if (raw.startsWith('\0')) {
current = raw.slice(1).trim();
} else {
const line = raw.trim();
if (line && current && !map.has(line)) map.set(line, current);
}
}
return map;
}
const lastmodMap = buildLastmodMap();

/** Maps a built page URL back to its source file's last commit date. */
function lastmodFor(url) {
const path = new URL(url).pathname.replace(/\/$/, '').replace(/^\//, '');
const candidates = [];
if (path === '') {
candidates.push('src/content/docs/index.mdx');
} else if (path.startsWith('changelog/post/')) {
const slug = path.slice('changelog/post/'.length);
for (const key of lastmodMap.keys()) {
if (key.startsWith('src/content/changelog/') && key.endsWith(`/${slug}.mdx`)) candidates.push(key);
}
} else {
for (const ext of ['md', 'mdx']) {
candidates.push(`src/content/docs/${path}.${ext}`, `src/content/docs/${path}/index.${ext}`);
}
}
for (const c of candidates) {
const date = lastmodMap.get(c);
if (date) return date;
}
return undefined;
}

/** Writes dist/_redirects (Cloudflare redirects file) from the map above.
Each path is emitted with and without a trailing slash so both forms 301. */
function emitRedirectsFile() {
Expand Down Expand Up @@ -73,6 +119,14 @@ export default defineConfig({

integrations: [
emitRedirectsFile(),
// Starlight skips its own sitemap integration when one is already present.
// This one adds <lastmod> from each page's last git commit date.
sitemap({
serialize(item) {
const lastmod = lastmodFor(item.url);
return lastmod ? { ...item, lastmod } : item;
},
}),
mermaid({ autoTheme: true }),
starlight({
title: 'StreamElements Docs',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"dependencies": {
"@astrojs/rss": "^4.0.19",
"@astrojs/sitemap": "^3.7.3",
"@astrojs/starlight": "^0.41.3",
"@fontsource-variable/inter": "^5.2.8",
"astro": "^7.1.0",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 41 additions & 3 deletions src/components/overrides/Head.astro
Original file line number Diff line number Diff line change
@@ -1,12 +1,50 @@
---
/**
* Head override: emits a <meta name="keywords"> tag for pages that
* declare `keywords` in their frontmatter (carried over from Docusaurus).
* Head override:
* - emits a <meta name="keywords"> tag for pages that declare `keywords`
* in their frontmatter (carried over from Docusaurus)
* - emits BreadcrumbList JSON-LD derived from the URL path, so search
* results show a breadcrumb trail instead of a bare URL
*/
import Default from '@astrojs/starlight/components/Head.astro';

const { keywords } = Astro.locals.starlightRoute.entry.data;
const { keywords, title } = Astro.locals.starlightRoute.entry.data;

// Labels for path segments that appear between the site root and the page.
const SECTION_LABELS: Record<string, string> = {
chatbot: 'Chatbot',
overlays: 'Overlays',
websockets: 'WebSockets',
commands: 'Commands',
default: 'Default Commands',
variables: 'Variables',
modules: 'Modules',
filters: 'Spam Filters',
topics: 'Topics',
};

const site = (Astro.site?.href ?? 'https://docs.streamelements.com/').replace(/\/$/, '');
const segments = Astro.url.pathname.split('/').filter(Boolean);

let breadcrumbs: object | null = null;
if (segments.length > 0 && segments[0] !== '404') {
const items = [{ '@type': 'ListItem', position: 1, name: 'StreamElements Docs', item: site }];
segments.forEach((seg, i) => {
const isLast = i === segments.length - 1;
const name = isLast
? title
: (SECTION_LABELS[seg] ?? seg.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' '));
items.push({
'@type': 'ListItem',
position: i + 2,
name,
item: `${site}/${segments.slice(0, i + 1).join('/')}`,
});
});
breadcrumbs = { '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: items };
}
---

<Default><slot /></Default>
{keywords && keywords.length > 0 && <meta name="keywords" content={keywords.join(', ')} />}
{breadcrumbs && <script type="application/ld+json" set:html={JSON.stringify(breadcrumbs)} />}
2 changes: 1 addition & 1 deletion src/content/docs/chatbot/filters/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: Spam Filters
sidebar:
label: Overview
order: 0
description: "StreamElements Chatbot spam filters automatically moderate unwanted chat messages: banned words, caps, emotes, links, paragraphs, symbols, spam, repetition, zalgo, and languages."
description: "StreamElements Chatbot spam filters automatically moderate unwanted chat messages: banned words, caps, emotes, links, spam, and more."
keywords:
- chatbot spam filters
- twitch chat moderation
Expand Down
2 changes: 1 addition & 1 deletion src/content/docs/chatbot/getting-started.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Getting Started
title: Getting Started with the Chatbot
description: "Get started with the StreamElements Chatbot: create your first custom command and set up your first automated timer."
keywords:
- StreamElements custom commands
Expand Down
3 changes: 2 additions & 1 deletion src/content/docs/chatbot/variables/args.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
---
title: "Argument tokens"
title: "Command Arguments"
description: "Access the words of a chat message in StreamElements Chatbot commands with the $(1), $(1:), and $(1:3) argument tokens."
keywords:
- command arguments
- arguments
- argument tokens
- chatbot commands
Expand Down
5 changes: 3 additions & 2 deletions src/content/docs/chatbot/variables/user.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
---
title: "$(user)"
description: "Reference for the StreamElements Chatbot $(user) variables: display name, points, ranks, and activity data for any user."
description: "Reference for the StreamElements Chatbot $(user) variables: display name, points, watchtime, ranks, and activity data for any user."
syntax: "$(user [username])"
arguments: optional
keywords:
- streamelements
- chatbot
- user variable
- watchtime
- streamer tools
- chat commands
platforms: [twitch, kick]
Expand Down Expand Up @@ -58,7 +59,7 @@ Every variable below accepts the same optional username argument, e.g. `$(user.p
| `$(user.lastmessage)` | User's last typed message in the chat | Outputs a single space if no message is stored |
| `$(user.lastseen)` | Time since the user was most recently seen in the viewer list or chat | Returned as a duration, e.g. `5 mins 30 secs`; `<not found>` if never recorded |
| `$(user.lastactive)` | Time since the user most recently typed a message in the chat | Returned as a duration, e.g. `5 mins 30 secs`; `<not found>` if never recorded |
| `$(user.time_online)` | Total time the user has spent watching the stream | Returned as a duration, e.g. `2 hours 30 mins` |
| `$(user.time_online)` | The user's total watchtime — time spent watching the stream | Returned as a duration, e.g. `2 hours 30 mins` |
| `$(user.time_online_rank)` | User's rank on the leaderboard for online time watched | Returned as rank/total, e.g. `5/283`; `<error>` if the lookup fails |
| `$(user.time_offline)` | Total time the user has spent in chat while the stream is offline | Returned as a duration, e.g. `2 hours 30 mins` |
| `$(user.time_offline_rank)` | User's rank on the leaderboard for offline time watched | Returned as rank/total, e.g. `5/283`; `<error>` if the lookup fails |
Expand Down
4 changes: 4 additions & 0 deletions src/content/docs/index.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
---
title: StreamElements Documentation
description: The official documentation for StreamElements — chatbot, overlays, realtime websockets, and the developer API.
# The default <title> would double the brand ("StreamElements Documentation | StreamElements Docs").
head:
- tag: title
content: StreamElements Documentation
template: splash
editUrl: false
lastUpdated: false
Expand Down
2 changes: 1 addition & 1 deletion src/content/docs/overlays/getting-started.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Getting Started
title: Getting Started with Overlays
description: Create an overlay with StreamElements - Custom widgets, alerts, and more for Twitch and YouTube Live.
keywords:
- StreamElements Overlays
Expand Down
6 changes: 4 additions & 2 deletions src/content/docs/overlays/widget-structure.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
title: Code Editor
description: The StreamElements custom code editor for widgets, covering the HTML, CSS, JS, and custom field definitions.
title: Widget Structure & Custom Fields
sidebar:
label: Code Editor
description: "The structure of a StreamElements custom widget in the Code Editor: the HTML, CSS, and JS tabs, and the JSON that defines custom fields."
---

This section describes the StreamElements Custom Code Editor and Widget's Structure.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Emote Combo Module
title: Chatbot Emote Combo
description: "Real-time notifications when an emote combo is broken in chat."
wsTopic: 'channel.chatbot.modules.emotecombo'
scope: 'bot:read'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Pyramid Module
title: Chatbot Pyramid
description: "Real-time notifications when a user successfully completes a pyramid in chat."
wsTopic: 'channel.chatbot.modules.pyramid'
scope: 'bot:read'
Expand Down
Loading