guardian_content_timeline
Analyze content trends over time for any topic using The Guardian's archives. Track publication patterns, identify peaks in coverage, and visualize how topics evolve across specified date ranges.
Instructions
Analyze content timeline for a topic over time showing trends and peaks
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Topic or search terms to analyze over time | |
| from_date | Yes | Start date (YYYY-MM-DD) | |
| to_date | Yes | End date (YYYY-MM-DD) | |
| interval | No | Time interval for analysis | |
| section | No | Filter by section (optional) |
Implementation Reference
- The primary handler function for the 'guardian_content_timeline' tool. It validates input parameters, generates time periods, queries the Guardian API for each period to count articles and sample headlines, computes peaks and trends, and formats a detailed timeline report with visualizations.export async function guardianContentTimeline(client: GuardianClient, args: any): Promise<string> { const params = ContentTimelineParamsSchema.parse(args); const fromDate = validateDate(params.from_date); const toDate = validateDate(params.to_date); if (!fromDate || !toDate) { throw new Error('Invalid date format. Use YYYY-MM-DD format.'); } const interval = params.interval || 'month'; // Generate time periods based on interval const periods = generateTimePeriods(fromDate, toDate, interval); let result = `Content Timeline for "${params.query}" (${fromDate} to ${toDate}):\n\n`; const timelineData: TimelineData[] = []; let totalArticles = 0; // Analyze each time period for (const period of periods) { const searchParams: Record<string, any> = { q: params.query, 'from-date': period.start, 'to-date': period.end, 'page-size': 10, // Get sample headlines 'show-fields': 'headline,firstPublicationDate', 'order-by': 'relevance' }; if (params.section) { searchParams.section = params.section; } try { const response = await client.search(searchParams); const articles = response.response.results; const count = response.response.total; const sampleHeadlines = articles.slice(0, 3).map(article => article.webTitle || 'Untitled'); timelineData.push({ period: period.label, count: count, sampleHeadlines: sampleHeadlines }); totalArticles += count; // Rate limiting: small delay between requests await new Promise(resolve => setTimeout(resolve, 100)); } catch (error) { timelineData.push({ period: period.label, count: 0, sampleHeadlines: [] }); } } // Find peak periods const maxCount = Math.max(...timelineData.map(d => d.count)); const peakPeriods = timelineData.filter(d => d.count === maxCount && d.count > 0); // Display timeline results result += `**Total Articles**: ${totalArticles}\n`; result += `**Analysis Period**: ${timelineData.length} ${interval}${timelineData.length !== 1 ? 's' : ''}\n\n`; if (peakPeriods.length > 0) { result += `**Peak Coverage** (${maxCount} articles):\n`; peakPeriods.forEach(peak => { result += `• ${peak.period}\n`; }); result += '\n'; } result += `**Timeline Breakdown**:\n`; timelineData.forEach(data => { const intensity = data.count === 0 ? '○' : data.count < maxCount * 0.3 ? '●' : data.count < maxCount * 0.7 ? '●●' : '●●●'; result += `${intensity} **${data.period}**: ${data.count} articles\n`; if (data.sampleHeadlines.length > 0) { data.sampleHeadlines.forEach(headline => { result += ` • ${headline}\n`; }); } result += '\n'; }); // Trend analysis if (timelineData.length >= 3) { const trend = analyzeTrend(timelineData); result += `**Trend Analysis**: ${trend}\n`; } return result; }
- src/types/guardian.ts:153-159 (schema)Zod schema used for input validation in the guardian_content_timeline handler, ensuring correct types and formats for query, dates, interval, and section.export const ContentTimelineParamsSchema = z.object({ query: z.string(), from_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), to_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), interval: z.enum(['day', 'week', 'month', 'quarter']).optional(), section: z.string().optional(), });
- src/tools/index.ts:21-40 (registration)Registration of the guardian_content_timeline tool handler within the tools registry, mapping the tool name to the imported handler function.export function registerTools(client: GuardianClient): Record<string, ToolHandler> { return { guardian_search: (args) => guardianSearch(client, args), guardian_get_article: (args) => guardianGetArticle(client, args), guardian_longread: (args) => guardianLongread(client, args), guardian_lookback: (args) => guardianLookback(client, args), guardian_browse_section: (args) => guardianBrowseSection(client, args), guardian_get_sections: (args) => guardianGetSections(client, args), guardian_search_tags: (args) => guardianSearchTags(client, args), guardian_search_by_length: (args) => guardianSearchByLength(client, args), guardian_search_by_author: (args) => guardianSearchByAuthor(client, args), guardian_find_related: (args) => guardianFindRelated(client, args), guardian_get_article_tags: (args) => guardianGetArticleTags(client, args), guardian_content_timeline: (args) => guardianContentTimeline(client, args), guardian_author_profile: (args) => guardianAuthorProfile(client, args), guardian_topic_trends: (args) => guardianTopicTrends(client, args), guardian_top_stories_by_date: (args) => guardianTopStoriesByDate(client, args), guardian_recommend_longreads: (args) => guardianRecommendLongreads(client, args), }; }
- src/index.ts:402-431 (schema)MCP protocol input schema definition for the guardian_content_timeline tool, used in ListTools response.name: 'guardian_content_timeline', description: 'Analyze content timeline for a topic over time showing trends and peaks', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Topic or search terms to analyze over time', }, from_date: { type: 'string', description: 'Start date (YYYY-MM-DD)', }, to_date: { type: 'string', description: 'End date (YYYY-MM-DD)', }, interval: { type: 'string', description: 'Time interval for analysis', enum: ['day', 'week', 'month', 'quarter'], }, section: { type: 'string', description: 'Filter by section (optional)', }, }, required: ['query', 'from_date', 'to_date'], }, },
- Helper function to generate time periods (day, week, month, quarter) between from_date and to_date for timeline analysis.function generateTimePeriods(fromDate: string, toDate: string, interval: string): TimelinePeriod[] { const periods: TimelinePeriod[] = []; const start = new Date(fromDate); const end = new Date(toDate); let current = new Date(start); while (current <= end) { let periodEnd = new Date(current); let label = ''; switch (interval) { case 'day': label = current.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); break; case 'week': periodEnd.setDate(current.getDate() + 6); if (periodEnd > end) periodEnd = new Date(end); label = `Week of ${current.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`; break; case 'month': periodEnd = new Date(current.getFullYear(), current.getMonth() + 1, 0); if (periodEnd > end) periodEnd = new Date(end); label = current.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); break; case 'quarter': const quarter = Math.floor(current.getMonth() / 3) + 1; periodEnd = new Date(current.getFullYear(), quarter * 3, 0); if (periodEnd > end) periodEnd = new Date(end); label = `Q${quarter} ${current.getFullYear()}`; break; } periods.push({ start: current.toISOString().substring(0, 10), end: periodEnd.toISOString().substring(0, 10), label: label }); // Move to next period switch (interval) { case 'day': current.setDate(current.getDate() + 1); break; case 'week': current.setDate(current.getDate() + 7); break; case 'month': current.setMonth(current.getMonth() + 1); current.setDate(1); break; case 'quarter': current.setMonth(current.getMonth() + 3); current.setDate(1); break; } } return periods; }