diff --git a/docs/docs/faq.mdx b/docs/docs/faq.mdx index 154d668b4e6..d38c39eadad 100644 --- a/docs/docs/faq.mdx +++ b/docs/docs/faq.mdx @@ -1,7 +1,63 @@ --- sidebar_position: 9 +title: Frequently Asked Questions +description: Common questions about Apache Superset including performance, database support, visualizations, and configuration. +keywords: [superset faq, superset questions, superset help, data visualization faq] --- +import FAQSchema from '@site/src/components/FAQSchema'; + + + # FAQ ## How big of a dataset can Superset handle? diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index 8cab4b41d86..f22a1ce846c 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -23,6 +23,7 @@ import type * as OpenApiPlugin from 'docusaurus-plugin-openapi-docs'; import { themes } from 'prism-react-renderer'; import remarkImportPartial from 'remark-import-partial'; import remarkLocalizeBadges from './plugins/remark-localize-badges.mjs'; +import remarkTechArticleSchema from './plugins/remark-tech-article-schema.mjs'; import * as fs from 'fs'; import * as path from 'path'; @@ -46,7 +47,7 @@ if (!versionsConfig.components.disabled) { sidebarPath: require.resolve('./sidebarComponents.js'), editUrl: 'https://github.com/apache/superset/edit/master/docs/components', - remarkPlugins: [remarkImportPartial, remarkLocalizeBadges], + remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema], admonitions: { keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'], extendDefaults: true, @@ -74,7 +75,7 @@ if (!versionsConfig.developer_portal.disabled) { sidebarPath: require.resolve('./sidebarTutorials.js'), editUrl: 'https://github.com/apache/superset/edit/master/docs/developer_portal', - remarkPlugins: [remarkImportPartial, remarkLocalizeBadges], + remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema], admonitions: { keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'], extendDefaults: true, @@ -180,6 +181,83 @@ const config: Config = { favicon: '/img/favicon.ico', organizationName: 'apache', projectName: 'superset', + + // SEO: Structured data (Organization, Software, WebSite with SearchAction) + headTags: [ + // SoftwareApplication schema + { + tagName: 'script', + attributes: { + type: 'application/ld+json', + }, + innerHTML: JSON.stringify({ + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: 'Apache Superset', + applicationCategory: 'BusinessApplication', + operatingSystem: 'Cross-platform', + description: 'Apache Superset is a modern, enterprise-ready business intelligence web application for data exploration and visualization.', + url: 'https://superset.apache.org', + license: 'https://www.apache.org/licenses/LICENSE-2.0', + author: { + '@type': 'Organization', + name: 'Apache Software Foundation', + url: 'https://www.apache.org/', + logo: 'https://www.apache.org/foundation/press/kit/asf_logo.png', + }, + offers: { + '@type': 'Offer', + price: '0', + priceCurrency: 'USD', + }, + featureList: [ + 'Interactive dashboards', + 'SQL IDE', + '40+ visualization types', + 'Semantic layer', + 'Role-based access control', + 'REST API', + ], + }), + }, + // WebSite schema with SearchAction (enables sitelinks search box in Google) + { + tagName: 'script', + attributes: { + type: 'application/ld+json', + }, + innerHTML: JSON.stringify({ + '@context': 'https://schema.org', + '@type': 'WebSite', + name: 'Apache Superset', + url: 'https://superset.apache.org', + potentialAction: { + '@type': 'SearchAction', + target: { + '@type': 'EntryPoint', + urlTemplate: 'https://superset.apache.org/search?q={search_term_string}', + }, + 'query-input': 'required name=search_term_string', + }, + }), + }, + // Preconnect hints for faster external resource loading + { + tagName: 'link', + attributes: { + rel: 'preconnect', + href: 'https://WR5FASX5ED-dsn.algolia.net', + crossorigin: 'anonymous', + }, + }, + { + tagName: 'link', + attributes: { + rel: 'preconnect', + href: 'https://analytics.apache.org', + }, + }, + ], themes: [ '@saucelabs/theme-github-codeblock', '@docusaurus/theme-mermaid', @@ -212,6 +290,19 @@ const config: Config = { }, }, ], + // SEO: Generate robots.txt during build + [ + require.resolve('./plugins/robots-txt-plugin.js'), + { + policies: [ + { + userAgent: '*', + allow: '/', + disallow: ['/api/v1/', '/_next/', '/static/js/*.map'], + }, + ], + }, + ], [ '@docusaurus/plugin-client-redirects', { @@ -373,7 +464,7 @@ const config: Config = { } return `https://github.com/apache/superset/edit/master/docs/${versionDocsDirPath}/${docPath}`; }, - remarkPlugins: [remarkImportPartial, remarkLocalizeBadges], + remarkPlugins: [remarkImportPartial, remarkLocalizeBadges, remarkTechArticleSchema], admonitions: { keywords: ['note', 'tip', 'info', 'warning', 'danger', 'resources'], extendDefaults: true, @@ -396,11 +487,57 @@ const config: Config = { theme: { customCss: require.resolve('./src/styles/custom.css'), }, + // SEO: Sitemap configuration with priorities + sitemap: { + lastmod: 'date', + changefreq: 'weekly', + priority: 0.5, + ignorePatterns: ['/tags/**'], + filename: 'sitemap.xml', + createSitemapItems: async (params) => { + const { defaultCreateSitemapItems, ...rest } = params; + const items = await defaultCreateSitemapItems(rest); + return items.map((item) => { + // Boost priority for key pages + if (item.url.includes('/docs/intro')) { + return { ...item, priority: 1.0, changefreq: 'daily' }; + } + if (item.url.includes('/docs/quickstart')) { + return { ...item, priority: 0.9, changefreq: 'weekly' }; + } + if (item.url.includes('/docs/installation/')) { + return { ...item, priority: 0.8, changefreq: 'weekly' }; + } + if (item.url.includes('/docs/databases')) { + return { ...item, priority: 0.8, changefreq: 'weekly' }; + } + if (item.url.includes('/docs/faq')) { + return { ...item, priority: 0.7, changefreq: 'monthly' }; + } + if (item.url === 'https://superset.apache.org/') { + return { ...item, priority: 1.0, changefreq: 'daily' }; + } + return item; + }); + }, + }, } satisfies Options, ], ], themeConfig: { + // SEO: OpenGraph and Twitter meta tags + metadata: [ + { name: 'keywords', content: 'data visualization, business intelligence, BI, dashboards, SQL, analytics, open source, Apache, charts, reporting' }, + { property: 'og:type', content: 'website' }, + { property: 'og:site_name', content: 'Apache Superset' }, + { property: 'og:image', content: 'https://superset.apache.org/img/superset-og-image.png' }, + { property: 'og:image:width', content: '1200' }, + { property: 'og:image:height', content: '630' }, + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'twitter:image', content: 'https://superset.apache.org/img/superset-og-image.png' }, + { name: 'twitter:site', content: '@ApacheSuperset' }, + ], colorMode: { defaultMode: 'dark', disableSwitch: false, diff --git a/docs/plugins/remark-tech-article-schema.mjs b/docs/plugins/remark-tech-article-schema.mjs new file mode 100644 index 00000000000..44c505ac3fb --- /dev/null +++ b/docs/plugins/remark-tech-article-schema.mjs @@ -0,0 +1,153 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Note: visit from unist-util-visit is available if needed for tree traversal + +/** + * Remark plugin that automatically injects TechArticle schema import and component + * into documentation MDX files based on frontmatter. + * + * This enables rich snippets for technical documentation in search results. + * + * Frontmatter options: + * - title: (required) Article headline + * - description: (required) Article description + * - keywords: (optional) Array of keywords + * - seo_proficiency: (optional) 'Beginner' or 'Expert', defaults to 'Beginner' + * - seo_schema: (optional) Set to false to disable schema injection + */ +export default function remarkTechArticleSchema() { + return (tree, file) => { + const frontmatter = file.data.frontMatter || {}; + + // Skip if explicitly disabled or missing required fields + if (frontmatter.seo_schema === false) { + return; + } + + // Only add schema if we have title and description + if (!frontmatter.title || !frontmatter.description) { + return; + } + + const title = frontmatter.title; + const description = frontmatter.description; + const keywords = Array.isArray(frontmatter.keywords) ? frontmatter.keywords : []; + const proficiencyLevel = frontmatter.seo_proficiency || 'Beginner'; + + // Create the import statement + const importNode = { + type: 'mdxjsEsm', + value: `import TechArticleSchema from '@site/src/components/TechArticleSchema';`, + data: { + estree: { + type: 'Program', + sourceType: 'module', + body: [ + { + type: 'ImportDeclaration', + specifiers: [ + { + type: 'ImportDefaultSpecifier', + local: { type: 'Identifier', name: 'TechArticleSchema' }, + }, + ], + source: { + type: 'Literal', + value: '@site/src/components/TechArticleSchema', + }, + }, + ], + }, + }, + }; + + // Create the component node for MDX + const componentNode = { + type: 'mdxJsxFlowElement', + name: 'TechArticleSchema', + attributes: [ + { + type: 'mdxJsxAttribute', + name: 'title', + value: title, + }, + { + type: 'mdxJsxAttribute', + name: 'description', + value: description, + }, + ...(keywords.length > 0 + ? [ + { + type: 'mdxJsxAttribute', + name: 'keywords', + value: { + type: 'mdxJsxAttributeValueExpression', + value: JSON.stringify(keywords), + data: { + estree: { + type: 'Program', + sourceType: 'module', + body: [ + { + type: 'ExpressionStatement', + expression: { + type: 'ArrayExpression', + elements: keywords.map((k) => ({ + type: 'Literal', + value: k, + })), + }, + }, + ], + }, + }, + }, + }, + ] + : []), + ...(proficiencyLevel !== 'Beginner' + ? [ + { + type: 'mdxJsxAttribute', + name: 'proficiencyLevel', + value: proficiencyLevel, + }, + ] + : []), + ], + children: [], + }; + + // Insert import at the beginning + tree.children.unshift(importNode); + + // Find the first heading and insert component after it + let insertIndex = 1; // Default: after import + for (let i = 1; i < tree.children.length; i++) { + if (tree.children[i].type === 'heading') { + insertIndex = i + 1; + break; + } + } + + tree.children.splice(insertIndex, 0, componentNode); + }; +} diff --git a/docs/plugins/robots-txt-plugin.js b/docs/plugins/robots-txt-plugin.js new file mode 100644 index 00000000000..0b9bf348a12 --- /dev/null +++ b/docs/plugins/robots-txt-plugin.js @@ -0,0 +1,83 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable @typescript-eslint/no-require-imports */ +const fs = require('fs'); +const path = require('path'); +/* eslint-enable @typescript-eslint/no-require-imports */ + +/** + * Docusaurus plugin to generate robots.txt during build + * Configuration is passed via plugin options + */ +module.exports = function robotsTxtPlugin(context, options = {}) { + const { siteConfig } = context; + const { + policies = [{ userAgent: '*', allow: '/' }], + additionalSitemaps = [], + } = options; + + return { + name: 'robots-txt-plugin', + + async postBuild({ outDir }) { + const sitemapUrl = `${siteConfig.url}/sitemap.xml`; + + // Build robots.txt content + const lines = []; + + // Add policies + for (const policy of policies) { + lines.push(`User-agent: ${policy.userAgent}`); + + if (policy.allow) { + const allows = Array.isArray(policy.allow) ? policy.allow : [policy.allow]; + for (const allow of allows) { + lines.push(`Allow: ${allow}`); + } + } + + if (policy.disallow) { + const disallows = Array.isArray(policy.disallow) ? policy.disallow : [policy.disallow]; + for (const disallow of disallows) { + lines.push(`Disallow: ${disallow}`); + } + } + + if (policy.crawlDelay) { + lines.push(`Crawl-delay: ${policy.crawlDelay}`); + } + + lines.push(''); // Empty line between policies + } + + // Add sitemaps + lines.push(`Sitemap: ${sitemapUrl}`); + for (const sitemap of additionalSitemaps) { + lines.push(`Sitemap: ${sitemap}`); + } + + // Write robots.txt + const robotsPath = path.join(outDir, 'robots.txt'); + fs.writeFileSync(robotsPath, lines.join('\n')); + + console.log('Generated robots.txt'); + }, + }; +}; diff --git a/docs/src/components/FAQSchema.tsx b/docs/src/components/FAQSchema.tsx new file mode 100644 index 00000000000..45a56b424b7 --- /dev/null +++ b/docs/src/components/FAQSchema.tsx @@ -0,0 +1,66 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { JSX } from 'react'; +import Head from '@docusaurus/Head'; + +interface FAQItem { + question: string; + answer: string; +} + +interface FAQSchemaProps { + faqs: FAQItem[]; +} + +/** + * Component that injects FAQPage JSON-LD structured data + * Use this on FAQ pages to enable rich snippets in search results + * + * @example + * + */ +export default function FAQSchema({ faqs }: FAQSchemaProps): JSX.Element | null { + // FAQPage schema requires a non-empty mainEntity array per schema.org specs + if (!faqs || faqs.length === 0) { + return null; + } + + const schema = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: faqs.map((faq) => ({ + '@type': 'Question', + name: faq.question, + acceptedAnswer: { + '@type': 'Answer', + text: faq.answer, + }, + })), + }; + + return ( + + + + ); +} diff --git a/docs/src/components/TechArticleSchema.tsx b/docs/src/components/TechArticleSchema.tsx new file mode 100644 index 00000000000..9a0fc049b4e --- /dev/null +++ b/docs/src/components/TechArticleSchema.tsx @@ -0,0 +1,91 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { JSX } from 'react'; +import Head from '@docusaurus/Head'; +import { useLocation } from '@docusaurus/router'; + +interface TechArticleSchemaProps { + title: string; + description: string; + datePublished?: string; + dateModified?: string; + keywords?: string[]; + proficiencyLevel?: 'Beginner' | 'Expert'; +} + +/** + * Component that injects TechArticle JSON-LD structured data for documentation pages. + * This helps search engines understand technical documentation content. + * + * @example + * + */ +export default function TechArticleSchema({ + title, + description, + datePublished, + dateModified, + keywords = [], + proficiencyLevel = 'Beginner', +}: TechArticleSchemaProps): JSX.Element { + const location = useLocation(); + const url = `https://superset.apache.org${location.pathname}`; + + const schema = { + '@context': 'https://schema.org', + '@type': 'TechArticle', + headline: title, + description, + url, + proficiencyLevel, + author: { + '@type': 'Organization', + name: 'Apache Superset Contributors', + url: 'https://github.com/apache/superset/graphs/contributors', + }, + publisher: { + '@type': 'Organization', + name: 'Apache Software Foundation', + url: 'https://www.apache.org/', + logo: { + '@type': 'ImageObject', + url: 'https://www.apache.org/foundation/press/kit/asf_logo.png', + }, + }, + mainEntityOfPage: { + '@type': 'WebPage', + '@id': url, + }, + ...(datePublished && { datePublished }), + ...(dateModified && { dateModified }), + ...(keywords.length > 0 && { keywords: keywords.join(', ') }), + }; + + return ( + + + + ); +} diff --git a/docs/static/img/superset-og-image.png b/docs/static/img/superset-og-image.png new file mode 100644 index 00000000000..830fefaa6b0 Binary files /dev/null and b/docs/static/img/superset-og-image.png differ