PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.4
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.4
4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 All 202 releases
← All changes | includes/Core/Settings.php +288 -18 4.9.0 → 4.9.4 View file →
@@ -283,8 +283,12 @@
283 283 'enable_breadcrumb_category' => true,
284 284 'enable_breadcrumb_title' => true,
285 285 'enable_sidebar_cat_list' => true,
286 286 'enable_print_icon' => true,
287 + 'print_enable_logo' => false,
288 + 'print_logo' => array(),
289 + 'print_enable_footer' => false,
290 + 'print_footer_text' => '',
287 291 'enable_tags' => true,
288 292 'email_feedback' => true,
289 293 'feedback_link_text' => __( 'Still stuck? How can we help?', 'betterdocs' ),
290 294 'reaction_feedback_text' => __( 'Thanks for your feedback', 'betterdocs' ),
@@ -344,8 +348,27 @@
344 348 'betterdocs_access_control_repeater' => array(),
345 349 'internal_knowledge_base_type' => 'basic',
346 350 'betterdocs_access_control_repeater_kb' => array(),
347 351 'enable_git_integration' => false,
352 + 'enable_ai_actions' => true,
353 + 'enable_markdown_endpoint' => true,
354 + 'ai_actions_button_label' => __( 'Copy page', 'betterdocs' ),
355 + 'ai_actions_prompt_template' => __( 'Read from {URL} so I can ask questions about it.', 'betterdocs' ),
356 + 'ai_actions_copy_page' => true,
357 + 'ai_actions_view_markdown' => true,
358 + 'ai_actions_chatgpt' => true,
359 + 'ai_actions_claude' => true,
360 + 'ai_actions_gemini' => true,
361 + 'ai_actions_perplexity' => false,
362 + 'ai_actions_grok' => false,
363 + 'enable_listen' => true,
364 + 'listen_button_label' => __( 'Listen', 'betterdocs' ),
365 + 'listen_show_speed' => true,
366 + // Words a synthetic voice gets through in a minute at rate 1.
367 + // Deliberately lower than the 200 the reading-time pill assumes — that
368 + // number describes silent reading. Only the player's duration estimate
369 + // uses this; see Core\Listen::WORDS_PER_MINUTE.
370 + 'listen_words_per_minute' => 180,
348 371 /**
349 372 * MCP master switch. Off by default; the toggle lives on the
350 373 * BetterDocs → MCP page, not in the settings tree, and writes
351 374 * through POST betterdocs/v1/settings.
@@ -729,21 +752,38 @@
729 752 betterdocs()->kbmigration->migrate();
730 753 }
731 754 $_settings = wp_parse_args( $_normalized_settings, $_old_settings );
732 755
733 - // Check if there are actual changes before saving.
734 - // update_option returns false when values serialize identically, which can happen
735 - // due to object caching or type normalization even when user made changes.
736 - $_has_changes = $_settings != $_old_settings;
756 + // Detect whether this save actually changes the effective settings.
757 + //
758 + // The stored option and the submitted payload are normalized differently:
759 + // an optional field can be ABSENT from storage yet arrive as '' (e.g.
760 + // Feedback URL), and array fields can be stored empty ( [] ) while their
761 + // normalized/default form is non-empty (e.g. Instant Answer's
762 + // display_ia_texonomy defaults to ['all']). Comparing the raw arrays
763 + // ( $_settings != $_old_settings ) therefore reported a phantom change on
764 + // every save, leaving the tab perpetually "dirty" and always toasting
765 + // "Changes Saved Successfully." instead of "There are no changes to be
766 + // saved." — see WPDevelopers/betterdocs-pro#78.
767 + //
768 + // Compare like-for-like instead: fill defaults on both sides and run both
769 + // through the same normalization, so semantically-equal states (absent vs
770 + // '', [] vs ['all'], 'on' vs true) collapse to identical values and only a
771 + // real edit registers. This is also more reliable than update_option()'s
772 + // return, which is false whenever values serialize identically under object
773 + // caching or type coercion even when the user did change something (#49).
774 + $_defaults = array_merge( $this->get_default(), $this->get_pro_defaults() );
775 + $_old_normalized = $this->get_normalized_values( wp_parse_args( $_old_settings, $_defaults ), $_defaults );
776 + $_new_normalized = $this->get_normalized_values( wp_parse_args( $_settings, $_defaults ), $_defaults );
777 + $_has_changes = $_new_normalized != $_old_normalized;
737 778
738 779 $_saved = $this->database->save( $this->base_key, $_settings );
739 780
740 781 do_action_ref_array( 'betterdocs::settings::saved', array( $_saved, $_settings, $_old_settings, &$this ) );
741 782
742 - // Return true if save succeeded OR if there were changes to attempt saving.
743 - // This handles cases where update_option returns false due to identical serialization
744 - // (e.g., object caching, type coercion during serialization).
745 - return $_saved || $_has_changes;
783 + // The success / no-changes toast reflects whether the user made a real
784 + // change, not update_option()'s (unreliable) return value.
785 + return $_has_changes;
746 786 }
747 787
748 788 public function views( $hook ) {
749 789 return betterdocs()->views->get( 'admin/settings' );
@@ -1396,8 +1436,44 @@
1396 1436 'enable_disable_text_active' => true,
1397 1437 'default' => 1,
1398 1438 'priority' => 3
1399 1439 ),
1440 + 'print_enable_logo' => array(
1441 + 'name' => 'print_enable_logo',
1442 + 'type' => 'toggle',
1443 + 'label' => __( 'Logo on Printed Doc', 'betterdocs' ),
1444 + 'label_subtitle' => __( 'Show a logo at the top of the printed / PDF page', 'betterdocs' ),
1445 + 'enable_disable_text_active' => true,
1446 + 'default' => 0,
1447 + 'priority' => 4
1448 + ),
1449 + 'print_logo' => array(
1450 + 'name' => 'print_logo',
1451 + 'type' => 'media',
1452 + 'value' => '',
1453 + 'label' => __( 'Print Logo', 'betterdocs' ),
1454 + 'label_subtitle' => __( 'Leave empty to use your site logo, or the site icon when no site logo is set', 'betterdocs' ),
1455 + 'priority' => 5,
1456 + 'rules' => Rules::is( 'print_enable_logo', true )
1457 + ),
1458 + 'print_enable_footer' => array(
1459 + 'name' => 'print_enable_footer',
1460 + 'type' => 'toggle',
1461 + 'label' => __( 'Footer on Printed Doc', 'betterdocs' ),
1462 + 'label_subtitle' => __( 'Show a footer on every page of the printed / PDF document', 'betterdocs' ),
1463 + 'enable_disable_text_active' => true,
1464 + 'default' => 0,
1465 + 'priority' => 6
1466 + ),
1467 + 'print_footer_text' => array(
1468 + 'name' => 'print_footer_text',
1469 + 'type' => 'textarea',
1470 + 'label' => __( 'Print Footer Text', 'betterdocs' ),
1471 + 'label_subtitle' => __( 'Leave empty to use the site name and current year', 'betterdocs' ),
1472 + 'default' => '',
1473 + 'priority' => 7,
1474 + 'rules' => Rules::is( 'print_enable_footer', true )
1475 + ),
1400 1476 'enable_tags' => array(
1401 1477 'name' => 'enable_tags',
1402 1478 'type' => 'toggle',
1403 1479 'label' => __( 'Tags', 'betterdocs' ),
@@ -1402,9 +1478,9 @@
1402 1478 'type' => 'toggle',
1403 1479 'label' => __( 'Tags', 'betterdocs' ),
1404 1480 'enable_disable_text_active' => true,
1405 1481 'default' => 1,
1406 - 'priority' => 4
1482 + 'priority' => 8
1407 1483 ),
1408 1484 'show_last_update_time' => array(
1409 1485 'name' => 'show_last_update_time',
1410 1486 'type' => 'toggle',
@@ -1410,9 +1486,9 @@
1410 1486 'type' => 'toggle',
1411 1487 'label' => __( 'Last Update Time', 'betterdocs' ),
1412 1488 'enable_disable_text_active' => true,
1413 1489 'default' => 1,
1414 - 'priority' => 5
1490 + 'priority' => 9
1415 1491 ),
1416 1492 'enable_navigation' => array(
1417 1493 'name' => 'enable_navigation',
1418 1494 'type' => 'toggle',
@@ -1418,9 +1494,9 @@
1418 1494 'type' => 'toggle',
1419 1495 'label' => __( 'Navigation', 'betterdocs' ),
1420 1496 'enable_disable_text_active' => true,
1421 1497 'default' => 1,
1422 - 'priority' => 6
1498 + 'priority' => 10
1423 1499 ),
1424 1500 'enable_comment' => array(
1425 1501 'name' => 'enable_comment',
1426 1502 'type' => 'toggle',
@@ -1426,9 +1502,9 @@
1426 1502 'type' => 'toggle',
1427 1503 'label' => __( 'Comment', 'betterdocs' ),
1428 1504 'enable_disable_text_active' => true,
1429 1505 'default' => '',
1430 - 'priority' => 7
1506 + 'priority' => 11
1431 1507 ),
1432 1508 'enable_credit' => array(
1433 1509 'name' => 'enable_credit',
1434 1510 'type' => 'toggle',
@@ -1434,9 +1510,9 @@
1434 1510 'type' => 'toggle',
1435 1511 'label' => __( 'Show Powered by BetterDocs', 'betterdocs' ),
1436 1512 'enable_disable_text_active' => true,
1437 1513 'default' => '',
1438 - 'priority' => 8
1514 + 'priority' => 12
1439 1515 ),
1440 1516 'reaction_feedback_text' => array(
1441 1517 'name' => 'reaction_feedback_text',
1442 1518 'type' => 'text',
@@ -1441,9 +1517,9 @@
1441 1517 'name' => 'reaction_feedback_text',
1442 1518 'type' => 'text',
1443 1519 'label' => __( 'Reaction Feedback Text', 'betterdocs' ),
1444 1520 'default' => __( 'Thanks for your feedback.', 'betterdocs' ),
1445 - 'priority' => 9
1521 + 'priority' => 13
1446 1522 ),
1447 1523 'enable_estimated_reading_time' => array(
1448 1524 'name' => 'enable_estimated_reading_time',
1449 1525 'type' => 'toggle',
@@ -1449,9 +1525,9 @@
1449 1525 'type' => 'toggle',
1450 1526 'label' => __( 'Estimated Reading Time', 'betterdocs' ),
1451 1527 'enable_disable_text_active' => true,
1452 1528 'default' => 0,
1453 - 'priority' => 10
1529 + 'priority' => 14
1454 1530 ),
1455 1531 'estimated_reading_time_title' => array(
1456 1532 'name' => 'estimated_reading_time_title',
1457 1533 'type' => 'text',
@@ -1456,9 +1532,9 @@
1456 1532 'name' => 'estimated_reading_time_title',
1457 1533 'type' => 'text',
1458 1534 'label' => __( 'Estimated Reading Time Title', 'betterdocs' ),
1459 1535 'default' => '',
1460 - 'priority' => 11,
1536 + 'priority' => 15,
1461 1537 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1462 1538 ),
1463 1539 'estimated_reading_time_text' => array(
1464 1540 'name' => 'estimated_reading_time_text',
@@ -1464,9 +1540,9 @@
1464 1540 'name' => 'estimated_reading_time_text',
1465 1541 'type' => 'text',
1466 1542 'label' => __( 'Estimated Reading Time Text', 'betterdocs' ),
1467 1543 'default' => __( 'min read', 'betterdocs' ),
1468 - 'priority' => 12,
1544 + 'priority' => 16,
1469 1545 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1470 1546 ),
1471 1547 'singular_estimated_reading_time_text' => array(
1472 1548 'name' => 'singular_estimated_reading_time_text',
@@ -1472,9 +1548,9 @@
1472 1548 'name' => 'singular_estimated_reading_time_text',
1473 1549 'type' => 'text',
1474 1550 'label' => __( 'Estimated Reading Time Text Singular', 'betterdocs' ),
1475 1551 'default' => __( 'min read', 'betterdocs' ),
1476 - 'priority' => 13,
1552 + 'priority' => 17,
1477 1553 'rules' => Rules::is( 'enable_estimated_reading_time', true )
1478 1554 )
1479 1555 )
1480 1556 ),
@@ -1739,8 +1815,202 @@
1739 1815 'priority' => 1,
1740 1816 'label' => __( 'Show Related Docs', 'betterdocs' ),
1741 1817 'enable_disable_text_active' => true,
1742 1818 'default' => false
1819 + )
1820 + ) )
1821 + ),
1822 + 'layout_single_doc_ai_actions' => array(
1823 + 'id' => 'layout_single_doc_ai_actions',
1824 + 'name' => 'layout_single_doc_ai_actions',
1825 + 'type' => 'section',
1826 + 'label' => __( 'AI Actions', 'betterdocs' ),
1827 + 'priority' => 8,
1828 + 'fields' => apply_filters( 'betterdocs_single_doc_ai_actions', array(
1829 + 'enable_ai_actions' => array(
1830 + 'name' => 'enable_ai_actions',
1831 + 'type' => 'toggle',
1832 + 'priority' => 1,
1833 + 'label' => __( 'Enable AI Actions', 'betterdocs' ),
1834 + 'label_subtitle' => __( 'Show a "Copy page" button beside the doc title so readers can copy the page as Markdown or open it in an AI assistant.', 'betterdocs' ),
1835 + 'enable_disable_text_active' => true,
1836 + 'default' => true
1837 + ),
1838 + 'enable_markdown_endpoint' => array(
1839 + 'name' => 'enable_markdown_endpoint',
1840 + 'type' => 'toggle',
1841 + 'priority' => 2,
1842 + 'label' => __( 'Serve Docs as Markdown', 'betterdocs' ),
1843 + // Plain text, not `<doc-url>.md`: the subtitle is rendered as HTML, so an
1844 + // angle-bracketed placeholder is swallowed as an unknown tag and the
1845 + // sentence read "Publish every doc at .md".
1846 + 'label_subtitle' => __( 'Publish every doc as Markdown at its address plus .md (for example /docs/getting-started.md), so AI assistants can read a clean copy. The Listen player uses it too. Required by "Copy page", "View as Markdown", and the Claude and Google AI Studio actions.', 'betterdocs' ),
1847 + 'enable_disable_text_active' => true,
1848 + 'default' => true
1849 + ),
1850 + // AI Actions and the Markdown endpoint are deliberately independent: the
1851 + // `.md` copies also serve AI assistants directly and the Listen player.
1852 + // So switching AI Actions off leaves Markdown on — say so where the
1853 + // admin is looking, rather than leaving a lone "Enabled" toggle under a
1854 + // feature that reads as switched off.
1855 + 'ai_actions_markdown_notice' => array(
1856 + 'name' => 'ai_actions_markdown_notice',
1857 + 'type' => 'html',
1858 + 'priority' => 3,
1859 + 'html' => sprintf(
1860 + '<div class="betterdocs-ai-actions-markdown-notice" style="margin:8px 0 0;padding:12px 16px;background:#f0f6ff;border-left:4px solid #5a6bff;border-radius:4px;font-size:13px;color:#202223;line-height:1.5;">%s</div>',
1861 + esc_html__( 'AI Actions is off, but your docs are still published as Markdown (.md) for AI assistants and the Listen player. Turn off "Serve Docs as Markdown" above if you don\'t want that.', 'betterdocs' )
1862 + ),
1863 + 'rules' => Rules::logicalRule( array(
1864 + Rules::is( 'enable_ai_actions', false ),
1865 + Rules::is( 'enable_markdown_endpoint', true )
1866 + ), 'and' )
1867 + ),
1868 + 'ai_actions_button_label' => array(
1869 + 'name' => 'ai_actions_button_label',
1870 + 'type' => 'text',
1871 + 'priority' => 4,
1872 + 'label' => __( 'Button Label', 'betterdocs' ),
1873 + 'default' => __( 'Copy page', 'betterdocs' ),
1874 + 'rules' => Rules::is( 'enable_ai_actions', true )
1875 + ),
1876 + 'ai_actions_prompt_template' => array(
1877 + 'name' => 'ai_actions_prompt_template',
1878 + 'type' => 'text',
1879 + 'priority' => 5,
1880 + 'label' => __( 'AI Prompt Template', 'betterdocs' ),
1881 + 'label_subtitle' => __( 'Sent to the AI assistant when a reader opens this doc there. Use {URL} for the doc address.', 'betterdocs' ),
1882 + 'default' => __( 'Read from {URL} so I can ask questions about it.', 'betterdocs' ),
1883 + 'rules' => Rules::is( 'enable_ai_actions', true )
1884 + ),
1885 + 'ai_actions_copy_page' => array(
1886 + 'name' => 'ai_actions_copy_page',
1887 + 'type' => 'toggle',
1888 + 'priority' => 6,
1889 + 'label' => __( 'Copy Page', 'betterdocs' ),
1890 + 'enable_disable_text_active' => true,
1891 + 'default' => true,
1892 + 'rules' => Rules::logicalRule( array(
1893 + Rules::is( 'enable_ai_actions', true ),
1894 + Rules::is( 'enable_markdown_endpoint', true )
1895 + ), 'and' )
1896 + ),
1897 + 'ai_actions_view_markdown' => array(
1898 + 'name' => 'ai_actions_view_markdown',
1899 + 'type' => 'toggle',
1900 + 'priority' => 7,
1901 + 'label' => __( 'View as Markdown', 'betterdocs' ),
1902 + 'enable_disable_text_active' => true,
1903 + 'default' => true,
1904 + 'rules' => Rules::logicalRule( array(
1905 + Rules::is( 'enable_ai_actions', true ),
1906 + Rules::is( 'enable_markdown_endpoint', true )
1907 + ), 'and' )
1908 + ),
1909 + 'ai_actions_chatgpt' => array(
1910 + 'name' => 'ai_actions_chatgpt',
1911 + 'type' => 'toggle',
1912 + 'priority' => 8,
1913 + 'label' => __( 'Open in ChatGPT', 'betterdocs' ),
1914 + 'enable_disable_text_active' => true,
1915 + 'default' => true,
1916 + 'rules' => Rules::is( 'enable_ai_actions', true )
1917 + ),
1918 + 'ai_actions_claude' => array(
1919 + 'name' => 'ai_actions_claude',
1920 + 'type' => 'toggle',
1921 + 'priority' => 9,
1922 + 'label' => __( 'Open in Claude', 'betterdocs' ),
1923 + 'enable_disable_text_active' => true,
1924 + 'default' => true,
1925 + 'rules' => Rules::logicalRule( array(
1926 + Rules::is( 'enable_ai_actions', true ),
1927 + Rules::is( 'enable_markdown_endpoint', true )
1928 + ), 'and' )
1929 + ),
1930 + 'ai_actions_gemini' => array(
1931 + 'name' => 'ai_actions_gemini',
1932 + 'type' => 'toggle',
1933 + 'priority' => 10,
1934 + 'label' => __( 'Open in Google AI Studio', 'betterdocs' ),
1935 + 'label_subtitle' => __( 'Gemini has no way to receive a prompt from a link, so this opens Google AI Studio instead. A Google account is required.', 'betterdocs' ),
1936 + 'enable_disable_text_active' => true,
1937 + 'default' => true,
1938 + 'rules' => Rules::logicalRule( array(
1939 + Rules::is( 'enable_ai_actions', true ),
1940 + Rules::is( 'enable_markdown_endpoint', true )
1941 + ), 'and' )
1942 + ),
1943 + 'ai_actions_perplexity' => array(
1944 + 'name' => 'ai_actions_perplexity',
1945 + 'type' => 'toggle',
1946 + 'priority' => 11,
1947 + 'label' => __( 'Open in Perplexity', 'betterdocs' ),
1948 + 'enable_disable_text_active' => true,
1949 + 'default' => false,
1950 + 'rules' => Rules::logicalRule( array(
1951 + Rules::is( 'enable_ai_actions', true ),
1952 + Rules::is( 'enable_markdown_endpoint', true )
1953 + ), 'and' )
1954 + ),
1955 + 'ai_actions_grok' => array(
1956 + 'name' => 'ai_actions_grok',
1957 + 'type' => 'toggle',
1958 + 'priority' => 12,
1959 + 'label' => __( 'Open in Grok', 'betterdocs' ),
1960 + 'enable_disable_text_active' => true,
1961 + 'default' => false,
1962 + 'rules' => Rules::logicalRule( array(
1963 + Rules::is( 'enable_ai_actions', true ),
1964 + Rules::is( 'enable_markdown_endpoint', true )
1965 + ), 'and' )
1966 + )
1967 + ) )
1968 + ),
1969 + 'layout_single_doc_listen' => array(
1970 + 'id' => 'layout_single_doc_listen',
1971 + 'name' => 'layout_single_doc_listen',
1972 + 'type' => 'section',
1973 + 'label' => __( 'Listen', 'betterdocs' ),
1974 + 'priority' => 9,
1975 + 'fields' => apply_filters( 'betterdocs_single_doc_listen', array(
1976 + 'enable_listen' => array(
1977 + 'name' => 'enable_listen',
1978 + 'type' => 'toggle',
1979 + 'priority' => 1,
1980 + 'label' => __( 'Enable Listen', 'betterdocs' ),
1981 + 'label_subtitle' => __( 'Show a "Listen" button beside the reading time. Pressed, it turns into a small audio player that reads the doc aloud using the visitor\'s browser — no audio files and no third-party service.', 'betterdocs' ),
1982 + 'enable_disable_text_active' => true,
1983 + 'default' => true
1984 + ),
1985 + 'listen_button_label' => array(
1986 + 'name' => 'listen_button_label',
1987 + 'type' => 'text',
1988 + 'priority' => 2,
1989 + 'label' => __( 'Button Label', 'betterdocs' ),
1990 + 'default' => __( 'Listen', 'betterdocs' ),
1991 + 'rules' => Rules::is( 'enable_listen', true )
1992 + ),
1993 + 'listen_show_speed' => array(
1994 + 'name' => 'listen_show_speed',
1995 + 'type' => 'toggle',
1996 + 'priority' => 3,
1997 + 'label' => __( 'Speed Control', 'betterdocs' ),
1998 + 'label_subtitle' => __( 'Let readers cycle the playback speed between 0.75× and 2×.', 'betterdocs' ),
1999 + 'enable_disable_text_active' => true,
2000 + 'default' => true,
2001 + 'rules' => Rules::is( 'enable_listen', true )
2002 + ),
2003 + 'listen_words_per_minute' => array(
2004 + 'name' => 'listen_words_per_minute',
2005 + 'type' => 'number',
2006 + 'priority' => 4,
2007 + 'label' => __( 'Words Per Minute', 'betterdocs' ),
2008 + 'label_subtitle' => __( 'Used to estimate the total playing time the player shows. Browser voices read at roughly 180 words a minute at normal speed; raise this if the countdown finishes early.', 'betterdocs' ),
2009 + 'default' => 180,
2010 + 'min' => 60,
2011 + 'max' => 400,
2012 + 'rules' => Rules::is( 'enable_listen', true )
1743 2013 )
1744 2014 ) )
1745 2015 )
1746 2016 )