UNPKG

next-blog-ai

Version:

AI-powered blog generation and content delivery for Next.js and other frameworks

995 lines (926 loc) 24.5 kB
// src/core/utils.ts function snakeToCamel(str) { return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); } function normalizeKeys(obj) { if (obj === null || obj === void 0) { return obj; } if (Array.isArray(obj)) { return obj.map((item) => normalizeKeys(item)); } if (typeof obj === "object") { const newObj = {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { const camelKey = snakeToCamel(key); newObj[camelKey] = normalizeKeys(obj[key]); } } return newObj; } return obj; } function createRequestId(endpoint, params) { const sortedParams = Object.keys(params).sort().map((key) => `${key}=${params[key]}`).join("&"); return `${endpoint}?${sortedParams}`; } function addHtmlStyles(html, styles) { if (!html) return html; if (html.includes("<style>")) { return html.replace( /<style>([\s\S]*?)<\/style>/, `<style>$1 ${styles}</style>` ); } return `<style>${styles}</style>${html}`; } // src/core/styles.ts var DEFAULT_BLOG_STYLES = ` /* Base styles */ .next-blog-ai-content { line-height: 1.6; color: inherit; max-width: 100%; margin: 0 auto; } /* Headings */ .next-blog-ai-content h1, .next-blog-ai-content h2, .next-blog-ai-content h3, .next-blog-ai-content h4, .next-blog-ai-content h5, .next-blog-ai-content h6 { margin-top: 2rem; margin-bottom: 1rem; font-weight: 600; line-height: 1.25; color: inherit; } .next-blog-ai-content h1 { font-size: 2.25rem; } .next-blog-ai-content h2 { font-size: 1.75rem; border-bottom: 1px solid #eee; padding-bottom: 0.3rem; } .next-blog-ai-content h3 { font-size: 1.5rem; } .next-blog-ai-content h4 { font-size: 1.25rem; } .next-blog-ai-content h5, .next-blog-ai-content h6 { font-size: 1rem; } /* Paragraphs and text */ .next-blog-ai-content p { margin-top: 0; margin-bottom: 1.5rem; } .next-blog-ai-content strong { font-weight: 600; } .next-blog-ai-content em { font-style: italic; } /* Lists */ .next-blog-ai-content ul, .next-blog-ai-content ol { margin-top: 0; margin-bottom: 1.5rem; padding-left: 2rem; } .next-blog-ai-content li { margin-bottom: 0.5rem; } .next-blog-ai-content li > ul, .next-blog-ai-content li > ol { margin-top: 0.5rem; margin-bottom: 0.5rem; } /* Links */ .next-blog-ai-content a { color: #0070f3; text-decoration: none; } .next-blog-ai-content a:hover { text-decoration: underline; } /* Blockquotes */ .next-blog-ai-content blockquote { margin: 1.5rem 0; padding: 0.5rem 1rem; border-left: 4px solid #ddd; color: #666; background-color: #f9f9f9; } .next-blog-ai-content blockquote > :first-child { margin-top: 0; } .next-blog-ai-content blockquote > :last-child { margin-bottom: 0; } /* Code blocks */ .next-blog-ai-content pre { margin: 1.5rem 0; padding: 1rem; overflow: auto; background-color: #f6f8fa; border-radius: 3px; font-size: 0.875rem; } .next-blog-ai-content code { font-size: 0.875rem; padding: 0.2em 0.4em; background-color: rgba(27, 31, 35, 0.05); border-radius: 3px; } .next-blog-ai-content pre code { padding: 0; background-color: transparent; } /* Tables */ .next-blog-ai-content table { width: 100%; border-collapse: collapse; margin: 1.5rem 0; } .next-blog-ai-content table th, .next-blog-ai-content table td { padding: 0.5rem 1rem; border: 1px solid #e1e4e8; } .next-blog-ai-content table th { background-color: #f6f8fa; font-weight: 600; text-align: left; } .next-blog-ai-content table tr:nth-child(even) { background-color: #f7f7f7; } /* Images */ .next-blog-ai-content img { max-width: 100%; height: auto; display: block; margin: 1.5rem auto; } .next-blog-ai-content figure { margin: 1.5rem auto; text-align: center; } .next-blog-ai-content figcaption { font-size: 0.875rem; color: #666; margin-top: 0.5rem; } /* Comments */ .next-blog-ai-comments { margin-top: 3rem; border-top: 1px solid #eee; padding-top: 2rem; } .next-blog-ai-comment { margin-bottom: 2rem; padding: 1rem; background-color: #f9f9f9; border-radius: 4px; } .next-blog-ai-comment-header { display: flex; justify-content: space-between; margin-bottom: 0.5rem; } .next-blog-ai-comment-author { font-weight: 600; } .next-blog-ai-comment-date { color: #666; font-size: 0.875rem; } .next-blog-ai-comment-content { line-height: 1.5; } /* Comment form */ .next-blog-ai-comment-form { margin-top: 2rem; } .next-blog-ai-comment-form label { display: block; margin-bottom: 0.5rem; font-weight: 600; } .next-blog-ai-comment-form input, .next-blog-ai-comment-form textarea { width: 100%; padding: 0.5rem; margin-bottom: 1rem; border: 1px solid #ddd; border-radius: 4px; } .next-blog-ai-comment-form textarea { min-height: 6rem; } .next-blog-ai-comment-form button { background-color: #0070f3; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-weight: 500; } .next-blog-ai-comment-form button:hover { background-color: #0060df; } `; var DEFAULT_BLOG_LIST_STYLES = ` /* Blog post list - Grid View (Default) */ .next-blog-ai-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem; margin: 2rem 0; } @media (max-width: 768px) { .next-blog-ai-list { grid-template-columns: 1fr; } } /* Post content padding for grid view */ .next-blog-ai-post-content { padding: 1.5rem; display: flex; flex-direction: column; } /* List View Styles */ .next-blog-ai-list.list-view { display: block; } .next-blog-ai-list.list-view .next-blog-ai-post-card { display: flex; flex-direction: row; margin-bottom: 2rem; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); transition: transform 0.2s, box-shadow 0.2s; } .next-blog-ai-list.list-view .next-blog-ai-post-card:hover { transform: translateY(-4px); box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); } .next-blog-ai-list.list-view .next-blog-ai-post-image { width: 250px; min-width: 250px; height: 180px; padding-bottom: 0; position: relative; background-color: #f0f0f0; } .next-blog-ai-list.list-view .next-blog-ai-post-image img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; } .next-blog-ai-list.list-view .next-blog-ai-post-content { flex: 1; padding: 1.5rem; display: flex; flex-direction: column; } /* Make sure grid view items display correctly */ .next-blog-ai-list:not(.list-view) .next-blog-ai-post-card { display: flex; flex-direction: column; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); transition: transform 0.2s, box-shadow 0.2s; } /* Responsive adjustments for list view */ @media (max-width: 768px) { .next-blog-ai-list.list-view .next-blog-ai-post-card { flex-direction: column; } .next-blog-ai-list.list-view .next-blog-ai-post-image { width: 100%; height: 0; padding-bottom: 56.25%; /* 16:9 aspect ratio */ } } .next-blog-ai-post-card { display: flex; flex-direction: column; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); transition: transform 0.2s, box-shadow 0.2s; } .next-blog-ai-post-title { font-size: 1.25rem; font-weight: 600; margin-top: 0; margin-bottom: 0.5rem; color: #111; } .next-blog-ai-post-title a { color: inherit; text-decoration: none; } .next-blog-ai-post-title a:hover { color: #0070f3; } .next-blog-ai-post-meta { font-size: 0.875rem; color: #666; margin-bottom: 1rem; display: flex; align-items: center; gap: 1rem; } .next-blog-ai-post-excerpt { margin-bottom: 1.5rem; flex-grow: 1; } .next-blog-ai-post-footer { margin-top: auto; display: flex; justify-content: space-between; align-items: center; } .next-blog-ai-read-more { color: #0070f3; text-decoration: none; font-weight: 500; font-size: 0.875rem; } .next-blog-ai-read-more:hover { text-decoration: underline; } .next-blog-ai-comments-count { font-size: 0.875rem; color: #666; } /* Pagination */ .next-blog-ai-pagination { display: flex; justify-content: center; margin: 3rem 0; gap: 0.5rem; } .next-blog-ai-pagination-item { display: inline-flex; align-items: center; justify-content: center; min-width: 2.5rem; height: 2.5rem; padding: 0 0.75rem; border-radius: 4px; background-color: #f0f0f0; color: #333; text-decoration: none; font-weight: 500; } .next-blog-ai-pagination-item:hover { background-color: #e0e0e0; } .next-blog-ai-pagination-item.active { background-color: #0070f3; color: white; } .next-blog-ai-pagination-item.disabled { color: #999; pointer-events: none; } `; // src/core/client.ts var DEFAULT_API_URL = "https://next-blog-ai.com/api/blog"; function getApiUrl() { if (typeof process !== "undefined" && process.env && process.env.NEXT_BLOG_AI_LOCAL_DEVELOPMENT === "true") { return process.env.NEXT_BLOG_AI_DEV_URL || "http://localhost:3000/api/blog"; } return DEFAULT_API_URL; } var DEFAULT_CACHE_TTL = 0; var _NextBlogAI = class _NextBlogAI { /** * Create a new instance of NextBlogAI client */ constructor(config) { var _a, _b; if (!config.apiKey) { throw new Error("API key is required"); } this.apiKey = config.apiKey; this.apiUrl = getApiUrl(); this.defaultFormat = config.defaultFormat || "html"; this.defaultBlogListDisplay = config.defaultBlogListDisplay || "grid"; this.defaultCacheTTL = config.defaultCacheTTL || DEFAULT_CACHE_TTL; this.cache = /* @__PURE__ */ new Map(); this.pendingRequests = /* @__PURE__ */ new Map(); this.customStyles = { blogContent: (_a = config.styles) == null ? void 0 : _a.blogContent, blogList: (_b = config.styles) == null ? void 0 : _b.blogList }; this.defaultFetchOptions = config.defaultFetchOptions; this.retryOptions = { ..._NextBlogAI.DEFAULT_RETRY_OPTIONS, ...config.retry }; } /** * Generate a cache key from the request parameters */ generateCacheKey(endpoint, params) { const sortedParams = Object.keys(params).sort().map((key) => `${key}=${params[key]}`).join("&"); return `${endpoint}?${sortedParams}`; } /** * Get data from cache if available and not expired */ getFromCache(cacheKey, options) { var _a; if (options == null ? void 0 : options.bypassCache) { return null; } const cachedData = this.cache.get(cacheKey); if (!cachedData) { return null; } const ttl = (_a = options == null ? void 0 : options.ttl) != null ? _a : this.defaultCacheTTL; const now = Date.now(); const expirationTime = cachedData.timestamp + ttl * 1e3; if (now > expirationTime) { this.cache.delete(cacheKey); return null; } return cachedData.data; } /** * Store data in cache */ storeInCache(cacheKey, data) { this.cache.set(cacheKey, { data, timestamp: Date.now() }); } /** * Calculate retry delay with exponential backoff and optional jitter */ getRetryDelay(attempt) { const { initialDelay, backoffFactor, maxDelay, jitter } = this.retryOptions; let delay = initialDelay * Math.pow(backoffFactor, attempt); delay = Math.min(delay, maxDelay); if (jitter) { const jitterFactor = 0.25; const randomFactor = 1 - jitterFactor + Math.random() * jitterFactor * 2; delay = delay * randomFactor; } return delay; } /** * Sleep for specified milliseconds */ async sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Make API request with proper error handling and retry mechanism */ async makeRequest(endpoint, params = {}, options = {}) { const method = options.method || "GET"; const requestParams = { ...params, api_key: this.apiKey }; let cacheKey = ""; if (method === "GET") { cacheKey = this.generateCacheKey(endpoint, requestParams); const cachedData = this.getFromCache(cacheKey, options.cache); if (cachedData) { return { data: cachedData }; } } if (method === "GET") { const requestId = createRequestId(endpoint, requestParams); const pendingRequest = this.pendingRequests.get(requestId); if (pendingRequest) { return pendingRequest; } const requestPromise = this.executeRequest( endpoint, method, requestParams, options, cacheKey ); this.pendingRequests.set(requestId, requestPromise); requestPromise.finally(() => { this.pendingRequests.delete(requestId); }); return requestPromise; } return this.executeRequest( endpoint, method, requestParams, options, cacheKey ); } /** * Execute the actual API request with retries */ async executeRequest(endpoint, method, requestParams, options, cacheKey) { var _a; let attempt = 0; const maxRetries = this.retryOptions.maxRetries; while (true) { try { const url = new URL(`${this.apiUrl}/${endpoint}`); if (method === "GET") { Object.entries(requestParams).forEach(([key, value]) => { if (value !== void 0 && value !== null) { url.searchParams.append(key, String(value)); } }); } const fetchOptions = { ...this.defaultFetchOptions, ...(_a = options.cache) == null ? void 0 : _a.fetchOptions }; const response = await fetch(url.toString(), { method, headers: { "Content-Type": "application/json" }, body: method !== "GET" ? JSON.stringify({ ...options.body, api_key: this.apiKey }) : void 0, // Apply framework-specific fetch options if present ...fetchOptions }); const isRetriableStatus = response.status >= 500 || response.status === 429 || // Too Many Requests response.status === 408; if (!response.ok) { const errorData = await response.json().catch(() => ({ error: "Unknown error" })); const error = { message: errorData.error || "An unknown error occurred", status: response.status, code: errorData.code || `HTTP_${response.status}` }; if (isRetriableStatus && attempt < maxRetries) { attempt++; const delay = this.getRetryDelay(attempt); await this.sleep(delay); continue; } return { error }; } const rawData = await response.json(); const normalizedData = normalizeKeys(rawData); if (endpoint === "list" && normalizedData && typeof normalizedData === "object") { const listData = normalizedData; if (listData.format === "html" && "content" in listData) { const styles = this.customStyles.blogList || DEFAULT_BLOG_LIST_STYLES; const displayFormat = requestParams.display || this.defaultBlogListDisplay; const displayClass = displayFormat === "list" ? "list-view" : "grid-view"; listData.content = listData.content.replace( 'class="next-blog-ai-list"', `class="next-blog-ai-list ${displayClass}"` ); listData.content = addHtmlStyles(listData.content, styles); } } else if (endpoint === "post" && normalizedData && typeof normalizedData === "object") { const postData = normalizedData; if (postData.format === "html" && "content" in postData) { const styles = this.customStyles.blogContent || DEFAULT_BLOG_STYLES; postData.content = addHtmlStyles(postData.content, styles); if ("comments" in postData && typeof postData.comments === "string") { postData.comments = addHtmlStyles(postData.comments, styles); } if ("commentForm" in postData && typeof postData.commentForm === "string") { postData.commentForm = addHtmlStyles( postData.commentForm, styles ); } } } if (method === "GET" && cacheKey) { this.storeInCache(cacheKey, normalizedData); } return { data: normalizedData }; } catch (err) { if (attempt < maxRetries) { attempt++; const delay = this.getRetryDelay(attempt); await this.sleep(delay); continue; } const error = { message: err instanceof Error ? err.message : "Network error occurred", status: 0, code: "NETWORK_ERROR" }; return { error }; } } } /** * Get a list of blog posts */ async getBlogPosts(options = {}) { const params = { page: options.page || 1, per_page: options.perPage || 10, format: options.format || this.defaultFormat, display: options.display || this.defaultBlogListDisplay }; return this.makeRequest("list", params, { cache: options.cache }); } /** * Get a single blog post by slug */ async getBlogPost(slug, options = {}) { if (!slug) { return { error: { message: "Slug is required", status: 400 } }; } const params = { slug, format: options.format || this.defaultFormat }; return this.makeRequest("post", params, { cache: options.cache }); } /** * Submit a comment on a blog post */ async submitComment(data) { if (!data.postId) { return { error: { message: "Post ID is required", status: 400 } }; } if (!data.authorName) { return { error: { message: "Author name is required", status: 400 } }; } if (!data.content) { return { error: { message: "Comment content is required", status: 400 } }; } return this.makeRequest( "comment", {}, { method: "POST", body: { post_id: data.postId, author_name: data.authorName, author_email: data.authorEmail, content: data.content } } ); } /** * Get only SEO metadata for a blog post by slug * Optimized version that doesn't fetch the full content */ async getBlogPostSEO(slug, options = {}) { if (!slug) { return { error: { message: "Slug is required", status: 400 } }; } const params = { slug, seo_only: true }; return this.makeRequest("post/seo", params, { cache: options.cache }); } /** * Get only SEO metadata for blog list * Optimized version that doesn't fetch the full content */ async getBlogListSEO(options = {}) { const params = { page: options.page || 1, per_page: options.perPage || 10, seo_only: true }; return this.makeRequest("list/seo", params, { cache: options.cache }); } /** * Get all blog post slugs * Useful for static site generation in frameworks like Next.js */ async getAllBlogSlugs(options = {}) { const params = {}; return this.makeRequest("slugs", params, { cache: options.cache }); } /** * Clear the entire cache */ clearCache() { this.cache.clear(); } /** * Clear a specific item from the cache */ clearCacheItem(endpoint, params) { const cacheKey = this.generateCacheKey(endpoint, params); this.cache.delete(cacheKey); } /** * Set custom styles for HTML content */ setStyles(styles) { if (styles.blogContent) { this.customStyles.blogContent = styles.blogContent; } if (styles.blogList) { this.customStyles.blogList = styles.blogList; } } }; /** * Default retry options */ _NextBlogAI.DEFAULT_RETRY_OPTIONS = { maxRetries: 3, initialDelay: 300, backoffFactor: 2, maxDelay: 1e4, jitter: true }; var NextBlogAI = _NextBlogAI; function createNextBlogAI(config) { const configWithDefaults = { ...config, defaultFetchOptions: config.defaultFetchOptions || { cache: "no-store", next: { revalidate: 0 } } }; return new NextBlogAI(configWithDefaults); } // src/frameworks/next.ts function mapNextCacheOptions(options) { if (!options) { return void 0; } return { ttl: options.revalidate, bypassCache: options.dynamic || options.revalidate === 0, // Include the framework-specific fetch options fetchOptions: { cache: options.dynamic || options.revalidate === 0 ? "no-store" : "force-cache", next: { revalidate: options.revalidate } }, next: options }; } function withNextCache(fn) { return function(client, ...args) { const lastArg = args[args.length - 1]; if (lastArg && typeof lastArg === "object" && "next" in lastArg) { const { next, ...rest } = lastArg; const options = { ...rest, cache: mapNextCacheOptions(next) }; args[args.length - 1] = options; } return fn(client, ...args); }; } async function getBlogPostsWithNextCache(client, options = {}) { const cacheOptions = mapNextCacheOptions(options.next); const { next, ...restOptions } = options; return client.getBlogPosts({ ...restOptions, cache: cacheOptions }); } async function getBlogPostWithNextCache(client, slug, options = {}) { const cacheOptions = mapNextCacheOptions(options.next); const { next, ...restOptions } = options; return client.getBlogPost(slug, { ...restOptions, cache: cacheOptions }); } async function getBlogPostSEOWithNextCache(client, slug, options = {}) { const cacheOptions = mapNextCacheOptions(options.next); const { next, ...restOptions } = options; return client.getBlogPostSEO(slug, { ...restOptions, cache: cacheOptions }); } async function getBlogListSEOWithNextCache(client, options = {}) { const cacheOptions = mapNextCacheOptions(options.next); const { next, ...restOptions } = options; return client.getBlogListSEO({ ...restOptions, cache: cacheOptions }); } async function getAllBlogSlugsWithNextCache(client, options = {}) { const cacheOptions = mapNextCacheOptions(options.next); const { next, ...restOptions } = options; return client.getAllBlogSlugs({ ...restOptions, cache: cacheOptions }); } function createNextBlogAIForNextJs(apiKey, defaultCacheOptions = { revalidate: 0 }) { const client = new NextBlogAI({ apiKey }); const mergeWithDefaults = (options) => { if (!defaultCacheOptions) return options; return { ...defaultCacheOptions, ...options }; }; return { client, getBlogPosts: (options = {}) => { const finalOptions = { ...options, next: options.next || mergeWithDefaults(options.next) }; return getBlogPostsWithNextCache(client, finalOptions); }, getBlogPost: (slug, options = {}) => { const finalOptions = { ...options, next: options.next || mergeWithDefaults(options.next) }; return getBlogPostWithNextCache(client, slug, finalOptions); }, getBlogPostSEO: (slug, options = {}) => { const finalOptions = { ...options, next: options.next || mergeWithDefaults(options.next) }; return getBlogPostSEOWithNextCache(client, slug, finalOptions); }, getBlogListSEO: (options = {}) => { const finalOptions = { ...options, next: options.next || mergeWithDefaults(options.next) }; return getBlogListSEOWithNextCache(client, finalOptions); }, getAllBlogSlugs: (options = {}) => { const finalOptions = { ...options, next: options.next || mergeWithDefaults(options.next) }; return getAllBlogSlugsWithNextCache(client, finalOptions); } }; } export { DEFAULT_BLOG_LIST_STYLES, DEFAULT_BLOG_STYLES, NextBlogAI, addHtmlStyles, createNextBlogAI, createNextBlogAIForNextJs, getAllBlogSlugsWithNextCache, getBlogListSEOWithNextCache, getBlogPostSEOWithNextCache, getBlogPostWithNextCache, getBlogPostsWithNextCache, normalizeKeys, withNextCache }; //# sourceMappingURL=out.js.map //# sourceMappingURL=index.mjs.map