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