JKT48Connect

News Detail

Get detailed JKT48 news content using JKT48Connect API

Introduction

The JKT48Connect News Detail API provides access to complete news article content including HTML formatted text. Use this endpoint to fetch full article details using the news ID obtained from the main news API.

Full Content

Get complete news article with HTML content and formatting.

ID-based Retrieval

Fetch specific news articles using their unique identifiers.

Rich Text Support

Handle HTML content with proper parsing and display.

Quick Start

Get News ID

First, fetch news list to get the article ID you want to read in detail.

const newsData = await getJKT48News();
const firstArticleId = newsData.news[0].id; // e.g., "1926"

Fetch News Detail

curl "https://v2.jkt48connect.my.id/api/jkt48/news/1926?apikey=YOUR_API_KEY"

Process HTML Content

Parse and display the rich HTML content safely in your application.

Endpoint Details

Base URL: https://v2.jkt48connect.my.id
Endpoint: /api/jkt48/news/{id}
Method: GET
Authentication: API Key required

Path Parameters:

  • id (required): News article ID from the news list API

Query Parameters:

  • apikey (required): Your API authentication key

Example:

GET /api/jkt48/news/1926?apikey=YOUR_API_KEY HTTP/1.1
Host: v2.jkt48connect.my.id

Returns detailed news article with HTML content:

{
  "author": "JKT48ConnectCORP - Valzyy",
  "_id": "685e7adedc1fff4e792c3d0a",
  "id": "1926",
  "content": "<p class=\"MsoNormal\" style=\"margin: 0mm; line-height: 16.8667px;\"><span lang=\"EN-GB\" style=\"line-height: 16.1px; color: #333333;\">Terima kasih atas dukungannya untuk JKT48.</span></p>\r\n<p class=\"MsoNormal\" style=\"margin: 0mm; line-height: 16.8667px;\"><span lang=\"EN-GB\" style=\"line-height: 16.1px; color: #333333;\">JKT48 ALL IN TOUR akan berkunjung ke kota Semarang, Cimahi, Yogyakarta, Solo dan Surabaya pada bulan Juli mendatang.</span></p>",
  "date": "2025-06-26T17:00:00.000Z",
  "label": "/images/icon.cat2.png",
  "title": "Pengumuman Mengenai Stage Activity dan Mini-Live Performance di JKT48 \"ALL IN TOUR\" 2025"
}

Implementation Examples

const API_KEY = 'YOUR_API_KEY';
const BASE_URL = 'https://v2.jkt48connect.my.id';

async function getNewsDetail(newsId) {
  try {
    const response = await fetch(
      `${BASE_URL}/api/jkt48/news/${newsId}?apikey=${API_KEY}`
    );
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Failed to fetch news detail:', error);
    throw error;
  }
}

// Clean and format HTML content
function cleanNewsContent(htmlContent) {
  // Remove excessive styling and clean up HTML
  return htmlContent
    .replace(/style="[^"]*"/g, '') // Remove inline styles
    .replace(/class="[^"]*"/g, '') // Remove classes
    .replace(/lang="[^"]*"/g, '') // Remove language attributes
    .replace(/\r\n/g, '\n') // Normalize line breaks
    .replace(/<span[^>]*>/g, '') // Remove span tags
    .replace(/<\/span>/g, '') // Remove closing span tags
    .trim();
}

// Extract plain text from HTML
function extractPlainText(htmlContent) {
  const tempDiv = document.createElement('div');
  tempDiv.innerHTML = htmlContent;
  return tempDiv.textContent || tempDiv.innerText || '';
}

// Format news detail for display
function formatNewsDetail(newsDetail) {
  const publishDate = new Date(newsDetail.date);
  const cleanContent = cleanNewsContent(newsDetail.content);
  const plainText = extractPlainText(cleanContent);
  
  return {
    id: newsDetail.id,
    title: newsDetail.title,
    category: getCategoryFromLabel(newsDetail.label),
    publishDate: publishDate.toLocaleDateString('id-ID', {
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    }),
    htmlContent: cleanContent,
    plainText: plainText,
    wordCount: plainText.split(/\s+/).length,
    readingTime: Math.ceil(plainText.split(/\s+/).length / 200), // ~200 words per minute
    summary: generateSummary(plainText)
  };
}

// Generate article summary
function generateSummary(text, maxLength = 200) {
  const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0);
  let summary = '';
  
  for (const sentence of sentences) {
    if (summary.length + sentence.length <= maxLength) {
      summary += sentence.trim() + '. ';
    } else {
      break;
    }
  }
  
  return summary.trim() || text.substring(0, maxLength) + '...';
}

// Get full news article with formatted content
async function getFormattedNewsArticle(newsId) {
  try {
    const newsDetail = await getNewsDetail(newsId);
    const formatted = formatNewsDetail(newsDetail);
    
    return {
      ...formatted,
      meta: {
        author: newsDetail.author,
        publishedAt: newsDetail.date,
        category: getCategoryFromLabel(newsDetail.label),
        isRecent: isRecentNews(newsDetail.date)
      }
    };
  } catch (error) {
    console.error('Error formatting news article:', error);
    return null;
  }
}

// Display news article
async function displayNewsArticle(newsId) {
  try {
    const article = await getFormattedNewsArticle(newsId);
    if (!article) return;
    
    console.log('=== JKT48 NEWS DETAIL ===');
    console.log(`Title: ${article.title}`);
    console.log(`Category: ${article.category}`);
    console.log(`Published: ${article.publishDate}`);
    console.log(`Reading Time: ${article.readingTime} minutes`);
    console.log(`Word Count: ${article.wordCount} words`);
    console.log('\n--- SUMMARY ---');
    console.log(article.summary);
    console.log('\n--- FULL CONTENT ---');
    console.log(article.plainText);
    
  } catch (error) {
    console.error('Error displaying article:', error);
  }
}

// Batch fetch multiple news details
async function batchFetchNewsDetails(newsIds) {
  const promises = newsIds.map(id => getNewsDetail(id));
  const results = await Promise.allSettled(promises);
  
  return results.map((result, index) => ({
    id: newsIds[index],
    status: result.status,
    data: result.status === 'fulfilled' ? result.value : null,
    error: result.status === 'rejected' ? result.reason : null
  }));
}
import requests
import re
from bs4 import BeautifulSoup
from datetime import datetime

API_KEY = 'YOUR_API_KEY'
BASE_URL = 'https://v2.jkt48connect.my.id'

def get_news_detail(news_id: str) -> dict:
    """Fetch detailed news content by ID"""
    url = f"{BASE_URL}/api/jkt48/news/{news_id}"
    params = {'apikey': API_KEY}
    
    try:
        response = requests.get(url, params=params)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"Error fetching news detail: {e}")
        raise

def clean_html_content(html_content: str) -> str:
    """Clean and format HTML content"""
    # Parse HTML with BeautifulSoup
    soup = BeautifulSoup(html_content, 'html.parser')
    
    # Remove all attributes from tags
    for tag in soup.find_all():
        tag.attrs = {}
    
    # Get clean HTML
    return str(soup)

def extract_plain_text(html_content: str) -> str:
    """Extract plain text from HTML"""
    soup = BeautifulSoup(html_content, 'html.parser')
    return soup.get_text(separator=' ', strip=True)

def format_news_detail(news_detail: dict) -> dict:
    """Format news detail for display"""
    publish_date = datetime.fromisoformat(news_detail['date'].replace('Z', '+00:00'))
    clean_content = clean_html_content(news_detail['content'])
    plain_text = extract_plain_text(clean_content)
    
    return {
        'id': news_detail['id'],
        'title': news_detail['title'],
        'category': get_category_from_label(news_detail['label']),
        'publish_date': publish_date.strftime('%d %B %Y'),
        'html_content': clean_content,
        'plain_text': plain_text,
        'word_count': len(plain_text.split()),
        'reading_time': max(1, len(plain_text.split()) // 200),  # ~200 words per minute
        'summary': generate_summary(plain_text)
    }

def generate_summary(text: str, max_length: int = 200) -> str:
    """Generate article summary"""
    sentences = re.split(r'[.!?]+', text)
    sentences = [s.strip() for s in sentences if s.strip()]
    
    summary = ''
    for sentence in sentences:
        if len(summary) + len(sentence) <= max_length:
            summary += sentence + '. '
        else:
            break
    
    return summary.strip() or text[:max_length] + '...'

def get_category_from_label(label: str) -> str:
    """Extract category from icon label"""
    categories = {
        'cat1': 'Member Updates',
        'cat2': 'Events & Concerts',
        'cat3': 'General',
        'cat4': 'Merchandise',
        'cat5': 'Special Events',
        'cat8': 'Member Status'
    }
    
    for key, category in categories.items():
        if key in label:
            return category
    return 'General'

def get_formatted_news_article(news_id: str) -> dict:
    """Get full news article with formatted content"""
    try:
        news_detail = get_news_detail(news_id)
        formatted = format_news_detail(news_detail)
        
        return {
            **formatted,
            'meta': {
                'author': news_detail['author'],
                'published_at': news_detail['date'],
                'category': get_category_from_label(news_detail['label'])
            }
        }
    except Exception as e:
        print(f"Error formatting news article: {e}")
        return None

def display_news_article(news_id: str):
    """Display formatted news article"""
    try:
        article = get_formatted_news_article(news_id)
        if not article:
            return
        
        print("=== JKT48 NEWS DETAIL ===")
        print(f"Title: {article['title']}")
        print(f"Category: {article['category']}")
        print(f"Published: {article['publish_date']}")
        print(f"Reading Time: {article['reading_time']} minutes")
        print(f"Word Count: {article['word_count']} words")
        print("\n--- SUMMARY ---")
        print(article['summary'])
        print("\n--- FULL CONTENT ---")
        print(article['plain_text'])
        
    except Exception as e:
        print(f"Error displaying article: {e}")

# Usage example
if __name__ == "__main__":
    # Example: Display news article with ID "1926"
    display_news_article("1926")
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "regexp"
    "strings"
    "time"
    
    "github.com/PuerkitoBio/goquery"
)

const (
    APIKey  = "YOUR_API_KEY"
    BaseURL = "https://v2.jkt48connect.my.id"
)

type NewsDetail struct {
    ID      string `json:"_id"`
    NewsID  string `json:"id"`
    Author  string `json:"author"`
    Content string `json:"content"`
    Date    string `json:"date"`
    Label   string `json:"label"`
    Title   string `json:"title"`
}

type FormattedNews struct {
    ID          string `json:"id"`
    Title       string `json:"title"`
    Category    string `json:"category"`
    PublishDate string `json:"publish_date"`
    HTMLContent string `json:"html_content"`
    PlainText   string `json:"plain_text"`
    WordCount   int    `json:"word_count"`
    ReadingTime int    `json:"reading_time"`
    Summary     string `json:"summary"`
}

func getNewsDetail(newsID string) (*NewsDetail, error) {
    url := fmt.Sprintf("%s/api/jkt48/news/%s?apikey=%s", BaseURL, newsID, APIKey)
    
    client := &http.Client{Timeout: 30 * time.Second}
    resp, err := client.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("API request failed with status: %d", resp.StatusCode)
    }
    
    var newsDetail NewsDetail
    err = json.NewDecoder(resp.Body).Decode(&newsDetail)
    return &newsDetail, err
}

func cleanHTMLContent(htmlContent string) string {
    // Remove inline styles and classes
    re1 := regexp.MustCompile(`style="[^"]*"`)
    re2 := regexp.MustCompile(`class="[^"]*"`)
    re3 := regexp.MustCompile(`lang="[^"]*"`)
    
    content := re1.ReplaceAllString(htmlContent, "")
    content = re2.ReplaceAllString(content, "")
    content = re3.ReplaceAllString(content, "")
    content = strings.ReplaceAll(content, "\r\n", "\n")
    
    return strings.TrimSpace(content)
}

func extractPlainText(htmlContent string) string {
    doc, err := goquery.NewDocumentFromReader(strings.NewReader(htmlContent))
    if err != nil {
        return ""
    }
    return strings.TrimSpace(doc.Text())
}

func generateSummary(text string, maxLength int) string {
    sentences := regexp.MustCompile(`[.!?]+`).Split(text, -1)
    summary := ""
    
    for _, sentence := range sentences {
        sentence = strings.TrimSpace(sentence)
        if sentence == "" {
            continue
        }
        
        if len(summary)+len(sentence) <= maxLength {
            summary += sentence + ". "
        } else {
            break
        }
    }
    
    result := strings.TrimSpace(summary)
    if result == "" && len(text) > maxLength {
        return text[:maxLength] + "..."
    }
    return result
}

func getCategoryFromLabel(label string) string {
    categories := map[string]string{
        "cat1": "Member Updates",
        "cat2": "Events & Concerts",
        "cat3": "General",
        "cat4": "Merchandise",
        "cat5": "Special Events",
        "cat8": "Member Status",
    }
    
    re := regexp.MustCompile(`cat(\d+)`)
    match := re.FindStringSubmatch(label)
    if len(match) > 1 {
        if category, exists := categories["cat"+match[1]]; exists {
            return category
        }
    }
    return "General"
}

func formatNewsDetail(newsDetail *NewsDetail) *FormattedNews {
    publishDate, _ := time.Parse(time.RFC3339, newsDetail.Date)
    cleanContent := cleanHTMLContent(newsDetail.Content)
    plainText := extractPlainText(cleanContent)
    wordCount := len(strings.Fields(plainText))
    readingTime := wordCount / 200
    if readingTime == 0 {
        readingTime = 1
    }
    
    return &FormattedNews{
        ID:          newsDetail.NewsID,
        Title:       newsDetail.Title,
        Category:    getCategoryFromLabel(newsDetail.Label),
        PublishDate: publishDate.Format("2 January 2006"),
        HTMLContent: cleanContent,
        PlainText:   plainText,
        WordCount:   wordCount,
        ReadingTime: readingTime,
        Summary:     generateSummary(plainText, 200),
    }
}

func displayNewsArticle(newsID string) {
    newsDetail, err := getNewsDetail(newsID)
    if err != nil {
        fmt.Printf("Error fetching news detail: %v\n", err)
        return
    }
    
    formatted := formatNewsDetail(newsDetail)
    
    fmt.Println("=== JKT48 NEWS DETAIL ===")
    fmt.Printf("Title: %s\n", formatted.Title)
    fmt.Printf("Category: %s\n", formatted.Category)
    fmt.Printf("Published: %s\n", formatted.PublishDate)
    fmt.Printf("Reading Time: %d minutes\n", formatted.ReadingTime)
    fmt.Printf("Word Count: %d words\n", formatted.WordCount)
    fmt.Println("\n--- SUMMARY ---")
    fmt.Println(formatted.Summary)
    fmt.Println("\n--- FULL CONTENT ---")
    fmt.Println(formatted.PlainText)
}

func main() {
    // Example: Display news article with ID "1926"
    displayNewsArticle("1926")
}

Content Processing

The news content includes HTML formatting that needs proper handling for display in different contexts.

Content Structure:

  • HTML Content: Rich formatted text with paragraphs, spans, and styling
  • Plain Text: Extracted text content without HTML tags
  • Metadata: Publication date, author, and category information

Processing Steps:

StepDescriptionPurpose
HTML CleaningRemove inline styles and attributesConsistent formatting
Text ExtractionConvert HTML to plain textSearch and analysis
Summary GenerationCreate article previewQuick overview
Reading TimeCalculate based on word countUser experience

Common Use Cases

// Create reading interface
function createNewsReader(newsId) {
  return getFormattedNewsArticle(newsId).then(article => {
    return {
      header: {
        title: article.title,
        category: article.category,
        publishDate: article.publishDate,
        readingTime: `${article.readingTime} min read`,
        wordCount: `${article.wordCount} words`
      },
      content: {
        summary: article.summary,
        htmlContent: article.htmlContent,
        plainText: article.plainText
      },
      navigation: {
        hasNext: true,
        hasPrevious: true
      }
    };
  });
}
// Archive news by date and category
function archiveNewsByDate(newsDetails) {
  const archive = {};
  
  newsDetails.forEach(article => {
    const date = new Date(article.meta.publishedAt);
    const yearMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
    
    if (!archive[yearMonth]) {
      archive[yearMonth] = {
        period: yearMonth,
        articles: [],
        categories: new Set()
      };
    }
    
    archive[yearMonth].articles.push(article);
    archive[yearMonth].categories.add(article.category);
  });
  
  return Object.values(archive).map(period => ({
    ...period,
    categories: Array.from(period.categories),
    articleCount: period.articles.length
  }));
}

Error Handling

async function safeGetNewsDetail(newsId) {
  try {
    const detail = await getNewsDetail(newsId);
    
    // Validate response structure
    if (!detail.content || !detail.title) {
      throw new Error('Invalid news detail structure');
    }
    
    // Validate content is not empty
    const plainText = extractPlainText(detail.content);
    if (plainText.length < 10) {
      console.warn(`News ${newsId} has very short content`);
    }
    
    return detail;
    
  } catch (error) {
    console.error(`Failed to fetch news detail ${newsId}:`, error);
    return {
      id: newsId,
      title: 'Content Unavailable',
      content: '<p>Unable to load news content.</p>',
      date: new Date().toISOString(),
      label: '/images/icon.cat3.png',
      author: 'System'
    };
  }
}

Integration Example

Combine news list and detail APIs for complete news experience:

// Complete news workflow
async function loadNewsWithDetails(page = 1, limit = 5) {
  // Get news list
  const newsData = await getJKT48News(page, limit);
  
  // Get details for each news item
  const newsWithDetails = await Promise.all(
    newsData.news.map(async (newsItem) => {
      const detail = await getNewsDetail(newsItem.id);
      return formatNewsDetail(detail);
    })
  );
  
  return {
    articles: newsWithDetails,
    pagination: {
      page: newsData.page,
      perPage: newsData.perpage,
      total: newsData.total_count
    }
  };
}

Get your API key from JKT48Connect and start building comprehensive news applications!

How is this guide?

Last updated on