Explorar el Código

chore(docs): Move Hugo site back into docs

We will now be able to generate the complete documentation website directly from this monorepo.
Michael Bromley hace 2 años
padre
commit
3aceae8298
Se han modificado 100 ficheros con 2600 adiciones y 75 borrados
  1. 1 0
      .eslintrc.js
  2. 6 0
      docs/.gitignore
  3. 5 0
      docs/archetypes/default.md
  4. 21 0
      docs/assets/scripts/alpine-components/popover.ts
  5. 13 0
      docs/assets/scripts/alpine-components/scroll-spy.ts
  6. 19 0
      docs/assets/scripts/alpine.types.ts
  7. 9 0
      docs/assets/scripts/docs-formatting.ts
  8. 42 0
      docs/assets/scripts/main.ts
  9. 21 0
      docs/assets/scripts/nav-menu.ts
  10. 44 0
      docs/assets/scripts/recent-search-recorder.ts
  11. 283 0
      docs/assets/scripts/search-widget.ts
  12. 87 0
      docs/assets/scripts/tabs.ts
  13. 71 0
      docs/assets/scripts/toc-highlighter.ts
  14. 16 0
      docs/assets/styles/_article.scss
  15. 73 0
      docs/assets/styles/_blog.scss
  16. 52 0
      docs/assets/styles/_fonts.scss
  17. 205 0
      docs/assets/styles/_markdown.scss
  18. 89 0
      docs/assets/styles/_menu.scss
  19. 25 0
      docs/assets/styles/_mixins.scss
  20. 84 0
      docs/assets/styles/_search-widget.scss
  21. 145 0
      docs/assets/styles/_shortcodes.scss
  22. 83 0
      docs/assets/styles/_syntax.scss
  23. 41 0
      docs/assets/styles/_toc.scss
  24. 2 0
      docs/assets/styles/_utils.scss
  25. 56 0
      docs/assets/styles/_variables.scss
  26. 216 0
      docs/assets/styles/main.scss
  27. 22 0
      docs/config.toml
  28. 1 1
      docs/content/deployment/deploying-admin-ui.md
  29. 5 6
      docs/content/developer-guide/customizing-models.md
  30. 1 1
      docs/content/developer-guide/error-handling.md
  31. 2 2
      docs/content/developer-guide/importing-product-data.md
  32. 1 1
      docs/content/developer-guide/job-queue/index.md
  33. 2 2
      docs/content/developer-guide/multi-tenant/index.md
  34. 4 4
      docs/content/developer-guide/overview/_index.md
  35. 2 2
      docs/content/developer-guide/payment-integrations/index.md
  36. 1 1
      docs/content/developer-guide/promotions.md
  37. 1 1
      docs/content/developer-guide/shipping.md
  38. 1 1
      docs/content/developer-guide/stock-control/index.md
  39. 1 1
      docs/content/developer-guide/taxes.md
  40. 2 2
      docs/content/developer-guide/uploading-files.md
  41. 1 1
      docs/content/developer-guide/vendure-worker.md
  42. 3 3
      docs/content/getting-started.md
  43. 0 13
      docs/content/graphql-api/admin/_index.md
  44. 0 13
      docs/content/graphql-api/shop/_index.md
  45. 1 1
      docs/content/plugins/_index.md
  46. 1 1
      docs/content/plugins/extending-the-admin-ui/custom-form-inputs/_index.md
  47. 1 1
      docs/content/plugins/plugin-architecture/_index.md
  48. 1 1
      docs/content/plugins/plugin-examples/using-job-queue-service.md
  49. 1 1
      docs/content/plugins/writing-a-vendure-plugin.md
  50. 12 0
      docs/content/searchindex/_index.md
  51. 1 1
      docs/content/storefront/building-a-storefront/_index.md
  52. 2 2
      docs/content/storefront/order-workflow/_index.md
  53. 3 3
      docs/content/storefront/shop-api-guide.md
  54. 2 2
      docs/content/user-guide/catalog/products.md
  55. 1 1
      docs/content/user-guide/localization/index.md
  56. 2 2
      docs/content/user-guide/promotions/index.md
  57. 1 1
      docs/content/user-guide/settings/countries-zones.md
  58. 2 2
      docs/content/user-guide/settings/global-settings.md
  59. 1 1
      docs/content/user-guide/settings/taxes.md
  60. 17 0
      docs/layouts/404.html
  61. 117 0
      docs/layouts/_default/baseof.html
  62. 21 0
      docs/layouts/_default/list.html
  63. 10 0
      docs/layouts/_default/single.html
  64. 33 0
      docs/layouts/partials/announcement-banner.html
  65. 24 0
      docs/layouts/partials/breadcrumbs-static.html
  66. 57 0
      docs/layouts/partials/breadcrumbs.html
  67. 3 0
      docs/layouts/partials/docs/brand.html
  68. 18 0
      docs/layouts/partials/docs/git-footer.html
  69. 9 0
      docs/layouts/partials/docs/html-head.html
  70. 3 0
      docs/layouts/partials/docs/menu-bundle.html
  71. 6 0
      docs/layouts/partials/docs/mobile-header.html
  72. 11 0
      docs/layouts/partials/docs/shared.html
  73. 6 0
      docs/layouts/partials/docs/toc.html
  74. 213 0
      docs/layouts/partials/footer.html
  75. 14 0
      docs/layouts/partials/light-triangle-pattern.html
  76. 36 0
      docs/layouts/partials/menu-filetree.html
  77. 27 0
      docs/layouts/partials/top-bar.html
  78. 13 0
      docs/layouts/searchindex/searchindex.html
  79. 11 0
      docs/layouts/shortcodes/alert.html
  80. 15 0
      docs/layouts/shortcodes/generation-info.html
  81. 3 0
      docs/layouts/shortcodes/gql-enum-values.html
  82. 3 0
      docs/layouts/shortcodes/gql-fields.html
  83. 3 0
      docs/layouts/shortcodes/image.html
  84. 3 0
      docs/layouts/shortcodes/member-description.html
  85. 26 0
      docs/layouts/shortcodes/member-info.html
  86. 21 0
      docs/layouts/shortcodes/pull-quote.html
  87. 4 0
      docs/layouts/shortcodes/shop-api-operation.html
  88. 3 0
      docs/layouts/shortcodes/tab.html
  89. 10 0
      docs/netlify.toml
  90. 37 0
      docs/package.json
  91. 7 0
      docs/postcss.config.js
  92. BIN
      docs/static/branding/cube-logo-300.png
  93. BIN
      docs/static/branding/cube-logo-800.png
  94. 25 0
      docs/static/branding/cube-logo-vector.svg
  95. 0 0
      docs/static/branding/wordmark-logo-vector.svg
  96. BIN
      docs/static/branding/wordmark-logo.png
  97. 0 0
      docs/static/branding/wordmark-vector.svg
  98. BIN
      docs/static/branding/wordmark.png
  99. BIN
      docs/static/favicon.ico
  100. BIN
      docs/static/fonts/Inter-Bold.woff2

+ 1 - 0
.eslintrc.js

@@ -27,6 +27,7 @@ module.exports = {
         '**/*.d.ts',
         '/packages/ui-devkit/scaffold/**/*',
         '/packages/dev-server/load-testing/**/*',
+        '/docs/layouts/**/*',
     ],
     parser: '@typescript-eslint/parser',
     parserOptions: {

+ 6 - 0
docs/.gitignore

@@ -0,0 +1,6 @@
+.idea
+node_modules
+public
+static/*.js*
+static/*.css*
+resources/_gen

+ 5 - 0
docs/archetypes/default.md

@@ -0,0 +1,5 @@
+---
+title: "{{ .Name | humanize | title }}"
+weight: 1
+---
+

+ 21 - 0
docs/assets/scripts/alpine-components/popover.ts

@@ -0,0 +1,21 @@
+import { AlpineContext, AlpineEvent } from "../alpine.types";
+
+export function popover(open = false, focus = false) {
+    let restoreEl: HTMLDivElement | undefined = undefined;
+
+    return {
+        open,
+        onEscape() {
+            this.open = false;
+            restoreEl?.focus();
+        },
+        onClosePopoverGroup(e: AlpineEvent) {
+            e.detail.contains(this.$el) && (this.open = false);
+        },
+
+        toggle(e: AlpineEvent) {
+            this.open = !this.open;
+            this.open ? restoreEl = e.currentTarget as HTMLDivElement : restoreEl?.focus()
+        },
+    } as AlpineContext;
+}

+ 13 - 0
docs/assets/scripts/alpine-components/scroll-spy.ts

@@ -0,0 +1,13 @@
+import { AlpineContext, AlpineEvent } from "../alpine.types";
+
+export function scrollSpy() {
+    return {
+        isScrolledToTop: true,
+        scrollY: 0,
+        onScroll() {
+            this.scrollY = window.scrollY;
+            this.isScrolledToTop = this.scrollY === 0;
+        },
+    } as AlpineContext;
+}
+

+ 19 - 0
docs/assets/scripts/alpine.types.ts

@@ -0,0 +1,19 @@
+declare global {
+    interface Window {
+        Components: any;
+    }
+}
+
+export interface AlpineContext {
+    $el?: Element;
+    $refs?: { [name: string]: Element; };
+    $event?: Event;
+    $dispatch?: (name: string, data: any) => void;
+    $watch?: (prop: string, handler: (value: any) => void) => void;
+    $nextTick?: (handler: () => void) => void;
+    [prop: string]: any;
+}
+
+export interface AlpineEvent extends Event {
+    detail: any;
+}

+ 9 - 0
docs/assets/scripts/docs-formatting.ts

@@ -0,0 +1,9 @@
+export function formatDocs() {
+
+    Array.from(document.querySelectorAll('h3')).forEach(h1 => {
+        const nextSibling = h1.nextElementSibling;
+        if (nextSibling && nextSibling.classList.contains('member-info')) {
+            h1.classList.add('member-title');
+        }
+    });
+}

+ 42 - 0
docs/assets/scripts/main.ts

@@ -0,0 +1,42 @@
+import { formatDocs } from './docs-formatting';
+// import { initGraphQlPlaygroundWidgets } from './graphql-playground-widget';
+import { initNavMenu } from './nav-menu';
+import { SearchWidget } from './search-widget';
+import { initTabs } from './tabs';
+import { TocHighlighter } from './toc-highlighter';
+import 'alpinejs';
+import { popover } from './alpine-components/popover';
+import { scrollSpy } from './alpine-components/scroll-spy';
+
+window.Components = {};
+window.Components.popover = popover;
+window.Components.scrollSpy = scrollSpy;
+
+// tslint:disable-next-line
+require('../styles/main.scss');
+
+document.addEventListener(
+    'DOMContentLoaded',
+    async () => {
+        const toc = document.querySelector('#TableOfContents') as HTMLDivElement;
+        const tocHighlighter = new TocHighlighter(toc);
+        tocHighlighter.highlight();
+
+        const searchInput = document.querySelector('#searchInput') as HTMLInputElement;
+        const autocompleteWrapper = document.querySelector('#autocomplete-wrapper') as HTMLDivElement;
+        const searchTrigger = document.querySelector('#searchInputTrigger') as HTMLInputElement;
+        const searchOverlay = document.querySelector('#searchOverlay') as HTMLDivElement;
+        if (searchTrigger) {
+            const searchWidget = new SearchWidget(
+                searchInput,
+                autocompleteWrapper,
+                searchOverlay,
+                searchTrigger,
+            );
+        }
+        initTabs();
+        initNavMenu();
+        formatDocs();
+    },
+    false,
+);

+ 21 - 0
docs/assets/scripts/nav-menu.ts

@@ -0,0 +1,21 @@
+export function initNavMenu() {
+    const sections = document.querySelectorAll('nav li.section');
+    sections.forEach(makeExpandable);
+
+    const activeLink = document.querySelector('nav a.active');
+    if (activeLink) {
+        activeLink.scrollIntoView({ block: 'center' });
+    }
+}
+
+function makeExpandable(section: Element) {
+    const icon = section.querySelector('.section-icon');
+    const sublist = section.querySelector('ul');
+
+    if (icon && sublist) {
+        icon.addEventListener('click', () => {
+            icon.classList.toggle('expanded');
+            sublist.classList.toggle('expanded');
+        });
+    }
+}

+ 44 - 0
docs/assets/scripts/recent-search-recorder.ts

@@ -0,0 +1,44 @@
+
+interface RecentSearchResult {
+    parent: string;
+    page: string;
+    title: string;
+    url: string;
+    timestamp: number;
+}
+
+const RECENT_SEARCHES_KEY = '__vendure_io_recent_searches__';
+
+export class RecentSearchRecorder {
+    private recentSearches: RecentSearchResult[] = [];
+    private readonly maxAgeMs = 5 * 24 * 60 * 60 * 1000; // 5 days
+    private readonly maxSize = 5;
+
+    get list() {
+        return this.recentSearches.slice().reverse();
+    }
+
+    init() {
+        try {
+            const data = window.localStorage.getItem(RECENT_SEARCHES_KEY) ?? '[]';
+            const parsed: RecentSearchResult[] = JSON.parse(data);
+            const now = Date.now();
+            this.recentSearches = parsed.filter(x => now - x.timestamp < this.maxAgeMs);
+        } catch (e) {
+            // ...
+        }
+    }
+
+    record(details: Omit<RecentSearchResult, 'timestamp'>) {
+        if (this.recentSearches.find(x => x.url === details.url)) {
+            return;
+        }
+        this.recentSearches.push({
+            ...details, timestamp: Date.now(),
+        });
+        if (this.maxSize < this.recentSearches.length) {
+            this.recentSearches.shift();
+        }
+        window.localStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(this.recentSearches));
+    }
+}

+ 283 - 0
docs/assets/scripts/search-widget.ts

@@ -0,0 +1,283 @@
+import fuzzy, { FilterResult } from 'fuzzy';
+import { RecentSearchRecorder } from './recent-search-recorder';
+
+type Section = 'developer-guide' | 'user-guide' | 'config' | 'gql';
+
+interface IndexItem {
+    section: Section;
+    title: string;
+    parent: string;
+    headings: string[];
+    url: string;
+}
+
+interface DenormalizedItem {
+    section: Section;
+    title: string;
+    parent: string;
+    heading: string;
+    url: string;
+}
+
+const KeyCode = {
+    UP: 38,
+    DOWN: 40,
+    ENTER: 13,
+    ESCAPE: 27,
+};
+
+/**
+ * This class implements the auto-suggest search box for searching the docs.
+ */
+export class SearchWidget {
+
+    private readonly MAX_RESULTS = 7;
+    private searchIndex: Promise<DenormalizedItem[]> | undefined;
+    private results: Array<FilterResult<DenormalizedItem>> = [];
+    private selectedIndex = -1;
+    private autocompleteDiv: HTMLDivElement;
+    private wrapperDiv: HTMLDivElement;
+    private listElement: HTMLUListElement;
+    private recentSearchRecorder: RecentSearchRecorder;
+
+    constructor(private inputElement: HTMLInputElement,
+                private autoCompleteWrapperElement: HTMLDivElement,
+                private overlayElement: HTMLDivElement,
+                private triggerElement: HTMLInputElement) {
+        this.attachAutocomplete();
+        this.recentSearchRecorder = new RecentSearchRecorder();
+        this.recentSearchRecorder.init();
+
+        inputElement.addEventListener('input', (e) => {
+            this.handleInput(e as KeyboardEvent);
+        });
+
+        inputElement.addEventListener('keydown', (e: KeyboardEvent) => {
+            const listItemCount = this.inputElement.value === '' ? this.recentSearchRecorder.list.length : this.results.length;
+            switch (e.keyCode) {
+                case KeyCode.UP:
+                    this.selectedIndex = this.selectedIndex === 0 ? listItemCount - 1 : this.selectedIndex - 1;
+                    e.preventDefault();
+                    break;
+                case KeyCode.DOWN:
+                    this.selectedIndex = this.selectedIndex === (listItemCount - 1) ? 0 : this.selectedIndex + 1;
+                    e.preventDefault();
+                    break;
+                case KeyCode.ENTER:
+                    const selected = this.autocompleteDiv.querySelector('li.selected a') as HTMLAnchorElement;
+                    if (selected) {
+                        selected.click();
+                        this.results = [];
+                    }
+                    break;
+                case KeyCode.ESCAPE:
+                    this.results = [];
+                    this.inputElement.blur();
+                    break;
+            }
+            this.render();
+            setTimeout(() => {
+                Array.from(this.listElement.querySelectorAll('.result')).forEach(titleEl => {
+                    titleEl.addEventListener('click', this.recordClickToHistory)
+                });
+            });
+        });
+
+        this.wrapperDiv.addEventListener('click', (e) => {
+            this.results = [];
+            this.render();
+        });
+
+        const openModal = () => {
+            this.overlayElement.click();
+            setTimeout(() => {
+                this.inputElement.value = this.triggerElement.value;
+                this.triggerElement.value = '';
+                this.inputElement.focus();
+            }, 50);
+            this.render();
+        }
+
+        this.triggerElement.addEventListener('click', openModal);
+        this.triggerElement.addEventListener('keypress', openModal);
+        window.addEventListener('keydown', e => {
+            if (e.ctrlKey && e.key === 'k') {
+                openModal();
+                e.preventDefault();
+            }
+        });
+    }
+
+    toggleActive() {
+        this.wrapperDiv.classList.toggle('focus');
+        if (this.wrapperDiv.classList.contains('focus')) {
+            this.inputElement.focus();
+        }
+    }
+
+    /**
+     * Groups the results by section and renders as a list
+     */
+    private render() {
+
+        const sections: Section[] = ['developer-guide', 'user-guide', 'gql', 'config'];
+        let html = '';
+        let i = 0;
+        if (this.inputElement.value === '' && this.recentSearchRecorder.list.length) {
+            html += `<li class="section recent">Recent</li>`;
+            html += this.recentSearchRecorder.list.map(({parent, page, title, url}) => {
+                const inner = `<div class="title"><span class="parent">${parent}</span> › ${page}</div><div class="heading">${title}</div>`;
+                const selected = i === this.selectedIndex ? 'selected' : '';
+                i++;
+                return `<li class="${selected} result recent"><a href="${url}">${inner}</a></li>`;
+            }).join('\n');
+        } else {
+            for (const sec of sections) {
+                const matches = this.results.filter(r => r.original.section === sec);
+                if (matches.length) {
+                    const sectionName = this.getSectionName(sec);
+                    html += `<li class="section ${sec}">${sectionName}</li>`;
+                }
+                html += matches.map((result) => {
+                    const {section, title, parent, heading, url} = result.original;
+                    const anchor = heading !== title ? '#' + this.headingToAnchor(heading) : '';
+                    const inner = `<div class="title"><span class="parent">${parent}</span> › <span class="page">${title}</span></div><div class="heading">${result.string}</div>`;
+                    const selected = i === this.selectedIndex ? 'selected' : '';
+                    i++;
+                    return `<li class="${selected} result ${sec}"><a href="${url + anchor}">${inner}</a></li>`;
+                }).join('\n');
+            }
+        }
+
+        this.listElement.innerHTML = html;
+    }
+
+    private recordClickToHistory = (e: Event) => {
+        const title = (e.currentTarget as HTMLDivElement).querySelector('.heading')?.textContent;
+        const parent = (e.currentTarget as HTMLDivElement).querySelector('.parent')?.textContent;
+        const page = (e.currentTarget as HTMLDivElement).querySelector('.page')?.textContent;
+        const url = (e.currentTarget as HTMLDivElement).querySelector('a')?.href;
+        if (title && url && parent && page) {
+            this.recentSearchRecorder.record({
+                parent,
+                page,
+                title,
+                url,
+            });
+        }
+    }
+
+    private getSectionName(sec: Section): string {
+        switch (sec) {
+            case 'developer-guide':
+                return 'Developer Guide';
+            case 'config':
+                return 'TypeScript API';
+            case 'gql':
+                return 'GraphQL API';
+            case 'user-guide':
+                return 'Administrator Guide';
+        }
+    }
+
+    private attachAutocomplete() {
+        this.autocompleteDiv = document.createElement('div');
+        this.autocompleteDiv.classList.add('autocomplete');
+        this.listElement = document.createElement('ul');
+        this.autocompleteDiv.appendChild(this.listElement);
+        this.wrapperDiv = this.autoCompleteWrapperElement;
+        this.wrapperDiv.classList.add('autocomplete-wrapper');
+
+        const parent = this.inputElement.parentElement;
+        if (parent) {
+            // parent.insertBefore(this.wrapperDiv, this.inputElement);
+            // this.wrapperDiv.appendChild(this.inputElement);
+            this.wrapperDiv.appendChild(this.autocompleteDiv);
+        }
+    }
+
+    private async handleInput(e: KeyboardEvent) {
+        const term = (e.target as HTMLInputElement).value.trim();
+        this.results = term ? await this.getResults(term) : [];
+        this.selectedIndex = 0;
+        this.render();
+    }
+
+    private async getResults(term: string) {
+        const items = await this.getSearchIndex();
+        const results = fuzzy.filter(
+            term,
+            items,
+            {
+                pre: '<span class="hl">',
+                post: '</span>',
+                extract(input: DenormalizedItem): string {
+                    return input.heading;
+                },
+            },
+        );
+
+        if (this.MAX_RESULTS < results.length) {
+            // limit the maximum number of results from a particular
+            // section to prevent other possibly relevant results getting
+            // buried.
+            const devGuides = results.filter(r => r.original.section === 'developer-guide');
+            const adminGuides = results.filter(r => r.original.section === 'user-guide');
+            const gql = results.filter(r => r.original.section === 'gql');
+            const config = results.filter(r => r.original.section === 'config');
+            let pool = [devGuides, adminGuides, gql, config].filter(p => p.length);
+            const balancedResults = [];
+            for (let i = 0; i < this.MAX_RESULTS; i++) {
+                const next = pool[i % pool.length].shift();
+                if (next) {
+                    balancedResults.push(next);
+                }
+                pool = [devGuides, adminGuides, gql, config].filter(p => p.length);
+            }
+            return balancedResults;
+        }
+        return results;
+    }
+
+    private getSearchIndex(): Promise<DenormalizedItem[]> {
+        if (!this.searchIndex) {
+            // tslint:disable:no-eval
+            this.searchIndex = fetch('/searchindex/index.html')
+                .then(res => res.text())
+                .then(res => eval(res))
+                .then((items: IndexItem[]) => {
+                    const denormalized: DenormalizedItem[] = [];
+                    for (const {section, title, parent, headings, url} of items) {
+                        denormalized.push({
+                            section,
+                            title,
+                            parent,
+                            heading: title,
+                            url,
+                        });
+                        if (headings.length) {
+                            for (const heading of headings) {
+                                if (heading !== title) {
+                                    denormalized.push({
+                                        section,
+                                        title,
+                                        parent,
+                                        heading,
+                                        url,
+                                    });
+                                }
+                            }
+                        }
+                    }
+                    return denormalized;
+                });
+        }
+        return this.searchIndex;
+    }
+
+    private headingToAnchor(heading: string): string {
+        return heading.toLowerCase()
+            .replace(/\s/g, '-')
+            .replace(/[:]/g, '');
+    }
+}

+ 87 - 0
docs/assets/scripts/tabs.ts

@@ -0,0 +1,87 @@
+type TabGroup = HTMLDivElement[];
+
+const TAB_CLASS = 'tab';
+const TAB_CONTROL_CLASS = 'tab-control';
+const TAB_CONTROL_WRAPPER_CLASS = 'tab-controls';
+const CONTAINER_CLASS = 'tab-container';
+const ACTIVE_CLASS = 'active';
+
+export function initTabs() {
+    const tabs = document.querySelectorAll<HTMLDivElement>(`.${TAB_CLASS}`);
+    const tabGroups = groupTabs(Array.from(tabs));
+    const containers = tabGroups.map(g => wrapTabGroup(g));
+    containers.forEach(container => {
+        container.addEventListener('click', e => {
+            const target = e.target as HTMLElement;
+            if (target.classList.contains(TAB_CONTROL_CLASS)) {
+                const tabId = target.dataset.id;
+                // deactivate all sibling tabs & controls
+                const tabsInGroup = Array.from(container.querySelectorAll(`.${TAB_CLASS}`));
+                const controlsInGroup = Array.from(container.querySelectorAll(`.${TAB_CONTROL_CLASS}`));
+                [...tabsInGroup, ...controlsInGroup].forEach(tab => tab.classList.remove(ACTIVE_CLASS));
+                // activate the newly selected tab & control
+                target.classList.add(ACTIVE_CLASS);
+                tabsInGroup.filter(t => t.id === tabId).forEach(tab => tab.classList.add(ACTIVE_CLASS));
+            }
+        });
+    });
+}
+
+/**
+ * Groups sibling tabs together.
+ */
+function groupTabs(tabs: HTMLDivElement[]): TabGroup[] {
+    const tabGroups: TabGroup[] = [];
+    const remainingTabs = tabs.slice();
+    let next: HTMLDivElement | undefined;
+    do {
+        next = remainingTabs.shift();
+        if (next) {
+            const group: TabGroup = [next];
+            let nextSibling = next.nextElementSibling;
+            while (nextSibling === remainingTabs[0]) {
+                if (nextSibling) {
+                    next = remainingTabs.shift();
+                    // tslint:disable-next-line:no-non-null-assertion
+                    group.push(next!);
+                }
+                // tslint:disable-next-line:no-non-null-assertion
+                nextSibling = next!.nextElementSibling;
+            }
+            tabGroups.push(group);
+        }
+    } while (next);
+    return tabGroups;
+}
+
+/**
+ * Wrap the tabs of the group in a container div and add the tab controls
+ */
+function wrapTabGroup(tabGroup: TabGroup): HTMLDivElement {
+    const tabControls = tabGroup.map(({ title }, i) => {
+        return `<button class="${TAB_CONTROL_CLASS} ${i === 0 ? ACTIVE_CLASS : ''}" data-id="${toId(title)}">${title}</button>`;
+    }).join('');
+    const wrapper = document.createElement('div');
+    wrapper.classList.add(CONTAINER_CLASS);
+    wrapper.innerHTML = `<div class="${TAB_CONTROL_WRAPPER_CLASS}">${tabControls}</div>`;
+
+    const parent = tabGroup[0].parentElement;
+    if (parent) {
+        parent.insertBefore(wrapper, tabGroup[0]);
+    }
+    tabGroup.forEach((tab, i) => {
+        tab.id = toId(tab.title);
+        if (i === 0) {
+            tab.classList.add(ACTIVE_CLASS);
+        }
+        wrapper.appendChild(tab);
+    });
+    return wrapper;
+}
+
+/**
+ * Generate a normalized ID based on the title
+ */
+function toId(title: string): string {
+    return 'tab-' + title.toLowerCase().replace(/[^a-zA-Z]/g, '-');
+}

+ 71 - 0
docs/assets/scripts/toc-highlighter.ts

@@ -0,0 +1,71 @@
+/**
+ * Highlights the current section in the table of contents when scrolling.
+ */
+export class TocHighlighter {
+    constructor(private tocElement: HTMLElement) {}
+
+    highlight() {
+        const article = document.querySelector('article');
+        if (this.tocElement && article) {
+            const headers: HTMLHeadingElement[] = Array.from(
+                article.querySelectorAll('h1[id],h2[id],h3[id],h4[id]'),
+            );
+
+            window.addEventListener('scroll', (e) => {
+                this.highlightCurrentSection(headers);
+            });
+            this.highlightCurrentSection(headers);
+        }
+    }
+
+    private highlightCurrentSection(headers: HTMLElement[]) {
+        const locationHash = location.hash;
+        Array.from(this.tocElement.querySelectorAll('.active')).forEach((el) =>
+            el.classList.remove('active'),
+        );
+
+        // tslint:disable:prefer-for-of
+        for (let i = 0; i < headers.length; i++) {
+            const header = headers[i];
+            const nextHeader = headers[i + 1];
+            const id = header.getAttribute('id') as string;
+            if (
+                !nextHeader ||
+                (nextHeader && window.scrollY < nextHeader.offsetTop)
+            ) {
+                this.highlightItem(id);
+                return;
+            }
+
+            const isCurrentTarget = `#${id}` === locationHash;
+            const currentTargetOffset = isCurrentTarget ? 90 : 0;
+            if (header.offsetTop + currentTargetOffset >= window.scrollY) {
+                this.highlightItem(id);
+                return;
+            }
+        }
+    }
+
+    private highlightItem(id: string) {
+        const tocItem = this.tocElement.querySelector(`[href="#${id}"]`) as HTMLAnchorElement;
+        if (tocItem) {
+            tocItem.classList.add('active');
+
+            // ensure the highlighted item is scrolled into view in the TOC menu
+            const padding = 12;
+            const tocHeight = this.tocElement.offsetHeight;
+            const tocScrollTop = this.tocElement.scrollTop;
+            const outOfRangeTop = tocItem?.offsetTop < tocScrollTop + padding;
+            const outofRangeBottom = tocHeight + tocScrollTop < tocItem.offsetTop + padding;
+            if (outOfRangeTop) {
+                // console.log('¬TOP');
+                this.tocElement.scrollTo({ top: tocItem.offsetTop - tocItem.offsetHeight - padding });
+            }
+            if (outofRangeBottom) {
+                // console.log('$BOTTOm');
+                const delta = tocItem.offsetTop - (tocHeight + tocScrollTop);
+                this.tocElement.scrollTo({ top: tocScrollTop + delta + tocItem.offsetHeight + padding });
+            }
+        }
+    }
+}

+ 16 - 0
docs/assets/styles/_article.scss

@@ -0,0 +1,16 @@
+@import "variables";
+
+.article-toc {
+    #TableOfContents > ul {
+        list-style-type: none;
+        padding-left: 0;
+
+        > li > ul {
+            list-style-type: disc;
+            color: $gray-600;
+            > li {
+                margin-bottom: $padding-8;
+            }
+        }
+    }
+}

+ 73 - 0
docs/assets/styles/_blog.scss

@@ -0,0 +1,73 @@
+@import "variables";
+
+.blog {
+    display: flex;
+    @media screen and (max-width: $md-breakpoint) {
+        display: block;
+    }
+
+    .book-toc {
+        flex: 1;
+    }
+}
+
+.posts-list {
+    min-height: 50vh;
+    padding: $padding-16 * 1.6;
+    min-width: $body-min-width;
+    max-width: $md-breakpoint;
+    margin: auto;
+
+    h2 {
+        font-size: 2em;
+        font-weight: 400;
+    }
+    h5 {
+        margin-top: -24px;
+    }
+    .featured-image {
+        max-width: 100%;
+    }
+    p {
+        line-height: 1.4em;
+    }
+}
+
+.left-gutter {
+    flex: 1;
+}
+
+.blog-post {
+
+    header {
+        overflow: hidden;
+        padding-bottom: $padding-16;
+        border-bottom: 1px dashed $gray-300;
+        h1 {
+            font-size: 3em;
+            font-weight: 400;
+            color: $brand-color;
+            margin-bottom: 12px;
+        }
+        @media screen and (max-width: $sm-breakpoint) {
+            h1 {
+                font-size: 2em;
+            }
+        }
+    }
+
+    .markdown > p:first-child {
+        font-size: 22px;
+        color: $gray-700;
+        @media screen and (max-width: $sm-breakpoint) {
+            font-size: 16px;
+        }
+    }
+
+    figure {
+        img {
+            max-width: 100%;
+        }
+    }
+}
+

+ 52 - 0
docs/assets/styles/_fonts.scss

@@ -0,0 +1,52 @@
+@font-face {
+  font-family: 'Didact Gothic';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(/fonts/didact-gothic-wordmark.woff2) format('woff2');
+}
+
+@font-face {
+  font-family: 'Lexend Deca';
+  font-style: normal;
+  font-weight: 300;
+  font-display: swap;
+  src: url(/fonts/LexendDeca-Light.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+@font-face {
+  font-family: 'Lexend Deca';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(/fonts/LexendDeca-Regular.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 300;
+  font-display: swap;
+  src: url(/fonts/Inter-Light.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(/fonts/Inter-Regular.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 700;
+  font-display: swap;
+  src: url(/fonts/Inter-Bold.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}

+ 205 - 0
docs/assets/styles/_markdown.scss

@@ -0,0 +1,205 @@
+@import 'variables';
+
+$block-border-radius: 4px;
+
+@mixin codelike {
+    display: inline-block;
+    border-radius: 3px;
+    font-family: 'Source Code Pro', monospace;
+    background-color: $gray-100;
+    border: 1px solid $gray-200;
+}
+
+.markdown {
+    line-height: 1.8;
+    color: $body-font-color;
+    background: $body-background;
+    font-family: 'Inter', sans-serif;
+    font-weight: $body-font-weight;
+
+    @media all and (min-width: $sm-breakpoint) {
+        margin-left: 12px;
+    }
+
+    h1 {
+       font-size: 1.875rem;
+       line-height: 2.25rem;
+    }
+
+    h1:first-of-type {
+        text-transform: capitalize;
+        font-weight: 500;
+        margin-top: 0;
+        margin-bottom: 0;
+        font-size: 2.8em;
+
+        @media all and (max-width: $sm-breakpoint){
+            font-size: 2em;
+        }
+    }
+
+    h1:not(:first-of-type) {
+        margin-top: 48px;
+        padding: 3px 12px;
+        @include codelike;
+    }
+
+    h2 {
+        margin-top: 60px;
+        margin-bottom: 6px;
+        font-size: 1.8em;
+    }
+
+    h3 {
+        margin-top: 48px;
+        font-size: 1.4em;
+    }
+
+    h3.member-title {
+        margin-top: 6px;
+        margin-left: 6px;
+        padding: 2px 6px;
+        @include codelike;
+    }
+
+    h4 {
+        margin-top: 32px;
+    }
+
+    h1, h2, h3, h4, h5 {
+        font-family: $brand-font-face;
+        letter-spacing: -0.025em;
+        font-weight: bold;
+        line-height: 1.25;
+
+        &[id]:target {
+            scroll-margin-top: 60px;
+            text-decoration: underline;
+        }
+    }
+
+    p {
+        margin: 16px 0;
+    }
+
+    b, optgroup, strong {
+        font-weight: 700;
+    }
+
+    a {
+        text-decoration: none;
+        color: $color-link;
+
+        &:hover {
+            text-decoration: underline;
+        }
+
+        &:visited {
+            color: $color-visited-link;
+        }
+    }
+
+    ul, ol:not(.breadcrumbs) {
+        list-style-type: initial;
+        padding-left: 40px;
+    }
+
+    ol:not(.breadcrumbs) {
+        list-style-type: decimal;
+    }
+
+    li {
+        margin-bottom: 6px;
+    }
+
+    li::marker {
+        color: $gray-600;
+    }
+
+    code:not([data-lang]) {
+        font-family: 'Source Code Pro', monospace;
+        padding: 0 $padding-4;
+        background: $gray-200;
+        color: $gray-700;
+        font-size: 0.9em;
+        border-radius: $block-border-radius;
+        border: 1px solid $gray-300;
+    }
+
+    a > code:not([data-lang]) {
+        color: $color-link;
+    }
+
+    pre:not(.chroma) {
+        padding: $padding-16;
+        background: $color-code-bg;
+        border-radius: $block-border-radius;
+        font-size: $font-size-14;
+        overflow-x: auto;
+
+        code {
+            color: $color-code-text;
+            background: none;
+            padding: none;
+        }
+    }
+
+    blockquote {
+        margin: 0;
+        padding: $padding-16 $padding-16 * 2;
+        position: relative;
+        // font-size: 22px;
+        color: $gray-700;
+        border-left: 2px solid $gray-300;
+
+        :first-child { margin-top: 0; }
+        :last-child { margin-bottom: 0; }
+
+        &::before {
+            content: '“';
+            position: absolute;
+            font-family: Georgia, 'Times New Roman', Times, serif;
+            font-size: 48px;
+            top: 0px;
+            left: 10px;
+            color: $gray-500;
+        }
+    }
+
+    table {
+        width: 100%;
+        th {
+            text-align: left;
+        }
+        td, th {
+            padding: $padding-4;
+        }
+        tr:nth-child(odd) td {
+            background-color: $gray-100;
+        }
+    }
+
+    figure {
+        margin: $padding-16 0;
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+
+        img {
+            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+            border-radius: 0.5rem;
+        }
+        &.flat {
+            img {
+                box-shadow: none;
+            }
+        }
+        figcaption p, figcaption h4 {
+            text-align: center;
+            font-size: $font-size-base;
+            margin-top: $padding-4;
+            margin-bottom: $padding-4;
+            font-style: italic;
+        }
+    }
+}

+ 89 - 0
docs/assets/styles/_menu.scss

@@ -0,0 +1,89 @@
+nav.book-menu {
+  text-transform: capitalize;
+
+  ul {
+    padding: 0;
+    margin: 0;
+    list-style: none;
+
+    li {
+      margin: 1em 0;
+    }
+
+    a {
+      display: block;
+    }
+
+    a:hover {
+      opacity: .5;
+    }
+
+    ul {
+      padding-left: $padding-16;
+    }
+  }
+
+  ul:not(.expanded) > .section {
+    &:nth-of-type(7) {
+      &::before {
+        position: absolute;
+        top: 0;
+        left: -1rem;
+        right: .5rem;
+        content: ' ';
+        border-top: 1px solid $gray-400;
+      }
+
+      padding-top: 1em;
+    }
+
+    &:nth-of-type(n+7) {
+      color: $gray-700;
+    }
+  }
+
+  .section {
+    position: relative;
+
+    .section-link {
+      display: flex;
+      align-items: center;
+
+      .section-icon {
+        width: 12px;
+        height: 12px;
+        transform: rotateZ(90deg);
+        opacity: 0.5;
+        margin-left: -15px;
+        margin-right: 3px;
+        cursor: pointer;
+        transition: opacity 0.2s;
+
+        &:hover {
+          opacity: 0.9;
+        }
+
+        &.expanded {
+          transform: rotateZ(180deg);
+        }
+      }
+
+      a {
+        flex: 1;
+      }
+    }
+
+    > ul {
+      max-height: 0px;
+      overflow: hidden;
+
+      &.expanded {
+        max-height: initial;
+
+        > li:last-child {
+          margin-bottom: 0;
+        }
+      }
+    }
+  }
+}

+ 25 - 0
docs/assets/styles/_mixins.scss

@@ -0,0 +1,25 @@
+@import "variables";
+
+@mixin code-block {
+    padding: 12px;
+    font-size: 14px;
+    border-radius: 3px;
+    overflow-x: auto;
+    border: 1px solid $color-code-border;
+    color: $color-code-text;
+    background-color: $color-code-bg;
+    position: relative;
+
+    code {
+        font-family: 'Source Code Pro', monospace;
+    }
+}
+
+@mixin code-block-lang {
+    position: absolute;
+    right: 3px;
+    top: 0;
+    font-size: 12px;
+    color: $gray-400;
+    text-transform: uppercase;
+}

+ 84 - 0
docs/assets/styles/_search-widget.scss

@@ -0,0 +1,84 @@
+@import "variables";
+
+.autocomplete {
+    ul {
+        padding: 0;
+        margin: 0;
+        list-style-type: none;
+    }
+    li {
+        background-color: transparent;
+        transition: background-color 0.2s;
+        border-left: 3px solid;
+
+        &.section {
+            padding: 3px 12px;
+
+            &.recent {
+                border-color: #e5e5e5;
+                color: #696969;
+            }
+
+            &.user-guide {
+                border-color: #34D399;
+                color: #064E3B;
+            }
+
+            &.developer-guide {
+                border-color: #60A5FA;
+                color: #1E3A8A;
+            }
+
+            &.config {
+                border-color: #9CA3AF;
+                color: #1F2937;
+            }
+
+            &.gql {
+                border-color: #9CA3AF;
+                color: #1F2937;
+            }
+        }
+
+        &.result {
+            transition: border-color 0.2s, background-color 0.2s;
+            padding: 12px;
+            &.recent {
+                border-color: #c5c5c5;
+            }
+            &.user-guide {
+                border-color: #A7F3D0;
+            }
+            &.developer-guide { border-color: #93C5FD; }
+            &.config { border-color: #D1D5DB; }
+            &.gql { border-color: #D1D5DB; }
+        }
+        &.result.selected {
+            border-color: #24c9fb;
+            background-color: #ebfcff;
+        }
+        &.selected {
+            background-color: $gray-200;
+        }
+        a {
+            display: flex;
+            align-items: center;
+        }
+        .title {
+            color: $gray-800;
+            width: 200px;
+            font-size: 12px;
+            margin-right: 6px;
+        }
+        .parent {
+            color: $gray-600;
+        }
+        .heading {
+            color: $gray-900;
+            .hl {
+                background-color: transparentize($brand-color, 0.85);
+                color: darken($brand-color, 40%);
+            }
+        }
+    }
+}

+ 145 - 0
docs/assets/styles/_shortcodes.scss

@@ -0,0 +1,145 @@
+@import "variables";
+@import "mixins";
+
+
+/**
+ Member info for generated docs
+ */
+.member-info {
+    display: flex;
+    align-items: center;
+    @media screen and (max-width: $sm-breakpoint) {
+        flex-direction: column;
+        align-items: flex-start;
+    }
+    margin-top: 4px;
+    margin-left: 8px;
+    font-size: $font-size-12;
+
+    .since {
+        display: flex;
+    }
+
+    .kind-label {
+        font-size: $font-size-12;
+        background-color: #eaf9f5;
+        color: #38979f;
+        border-radius: 4px;
+        padding: 0 6px;
+        margin-right: 12px;
+    }
+
+    .type, .default {
+        margin-right: 12px;
+        a:link, a:visited {
+            color: darken($brand-color, 20%);
+        }
+    }
+    .type {
+        code {
+        }
+    }
+    .label{
+        display: inline-block;
+        color: $gray-600;
+    }
+}
+
+/**
+ Member description for generated docs
+ */
+.member-description {
+    margin-top: 3px;
+    margin-left: 8px;
+    margin-bottom: 32px;
+}
+
+/**
+ Info on the generated document
+ */
+.generation-info {
+    font-size: $font-size-12;
+    border-top: 1px dashed $gray-400;
+    .label {
+        color: $gray-600;
+    }
+    .file {
+        margin-left: 12px;
+    }
+}
+
+/**
+ GraphQL field list
+ */
+.gql-fields {
+    @include code-block;
+    font-family: 'Oxygen Mono', monospace;
+    &::before {
+        content: 'sdl';
+        @include code-block-lang;
+    }
+    ul {
+        padding: 0;
+        margin: 0;
+        list-style-type: none;
+        font-family: 'Oxygen Mono', monospace;
+    }
+    em {
+        color: #776e71;
+    }
+    a:link, a:visited {
+        color: $brand-color;
+    }
+}
+
+/**
+ GraphQL enum values
+ */
+.gql-enum-values {
+    @include code-block;
+    &::before {
+        content: 'sdl';
+        @include code-block-lang;
+    }
+    max-height: 80vh;
+    overflow-y: auto;
+    ul {
+        padding: 0;
+        margin: 0;
+        list-style-type: none;
+        font-family: 'Oxygen Mono', monospace;
+    }
+    em {
+        color: #776e71;
+    }
+}
+
+/**
+ Tabs
+ */
+.tab-container {
+    border-left: 2px solid $gray-200;
+    padding-left: 6px;
+    margin-left: -8px;
+    margin-bottom: 32px;
+    .tab-controls {
+        button.tab-control {
+            color: $gray-600;
+            border: none;
+            padding: 3px 6px;
+            margin: 0 3px;
+            border-bottom: 2px solid transparent;
+            background: none;
+            &.active {
+                border-bottom-color: $brand-color;
+                color: $gray-700;
+            }
+        }
+    }
+    .tab {
+        display: none;
+        &.active {
+            display: block;
+        }
+    }
+}

+ 83 - 0
docs/assets/styles/_syntax.scss

@@ -0,0 +1,83 @@
+@import "mixins";
+@import "variables";
+
+/**
+ Styles for code block syntax highlighting. Extracted from the paraiso-dark theme and then modified.
+ */
+
+pre.chroma {
+    @include code-block;
+
+    > code::before {
+        content: attr(data-lang);
+        @include code-block-lang;
+    }
+}
+
+$comment: $gray-500;
+$keyword: #3ed0bd;
+$decorator: #d0ba6f;
+$name: #86e6ff;
+
+/* Background */ .chroma { color: #e7e9db; background-color: #2f1e2e }
+/* Error */ .chroma .err { color: #ef6155 }
+/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
+/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; }
+/* LineHighlight */ .chroma .hl { width: 100%;background-color: #26a9135c }
+/* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; }
+/* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em; }
+/* Keyword */ .chroma .k { color: $keyword }
+/* KeywordConstant */ .chroma .kc { color: $keyword }
+/* KeywordDeclaration */ .chroma .kd { color: $decorator }
+/* KeywordNamespace */ .chroma .kn { color: #5bc4bf }
+/* KeywordPseudo */ .chroma .kp { color: $keyword }
+/* KeywordReserved */ .chroma .kr { color: $keyword }
+/* KeywordType */ .chroma .kt { color: #fec418 }
+/* NameAttribute */ .chroma .na { color: #06b6ef }
+/* NameClass */ .chroma .nc { color: #fec418 }
+/* NameConstant */ .chroma .no { color: #ef6155 }
+/* NameDecorator */ .chroma .nd { color: #5bc4bf }
+/* NameException */ .chroma .ne { color: #ef6155 }
+/* NameFunction */ .chroma .nf { color: #06b6ef }
+/* NameNamespace */ .chroma .nn { color: #fec418 }
+/* NameOther */ .chroma .nx { color: $name }
+/* NameTag */ .chroma .nt { color: #5bc4bf }
+/* NameVariable */ .chroma .nv { color: #ef6155 }
+/* Literal */ .chroma .l { color: #f99b15 }
+/* LiteralDate */ .chroma .ld { color: #48b685 }
+/* LiteralString */ .chroma .s { color: #48b685 }
+/* LiteralStringAffix */ .chroma .sa { color: #48b685 }
+/* LiteralStringBacktick */ .chroma .sb { color: #48b685 }
+/* LiteralStringDelimiter */ .chroma .dl { color: #48b685 }
+/* LiteralStringDoc */ .chroma .sd { color: $comment }
+/* LiteralStringDouble */ .chroma .s2 { color: #48b685 }
+/* LiteralStringEscape */ .chroma .se { color: #f99b15 }
+/* LiteralStringHeredoc */ .chroma .sh { color: #48b685 }
+/* LiteralStringInterpol */ .chroma .si { color: #f99b15 }
+/* LiteralStringOther */ .chroma .sx { color: #48b685 }
+/* LiteralStringRegex */ .chroma .sr { color: #48b685 }
+/* LiteralStringSingle */ .chroma .s1 { color: #48b685 }
+/* LiteralStringSymbol */ .chroma .ss { color: #48b685 }
+/* LiteralNumber */ .chroma .m { color: #f99b15 }
+/* LiteralNumberBin */ .chroma .mb { color: #f99b15 }
+/* LiteralNumberFloat */ .chroma .mf { color: #f99b15 }
+/* LiteralNumberHex */ .chroma .mh { color: #f99b15 }
+/* LiteralNumberInteger */ .chroma .mi { color: #f99b15 }
+/* LiteralNumberIntegerLong */ .chroma .il { color: #f99b15 }
+/* LiteralNumberOct */ .chroma .mo { color: #f99b15 }
+/* Operator */ .chroma .o { color: #5bc4bf }
+/* OperatorWord */ .chroma .ow { color: #5bc4bf }
+/* Comment */ .chroma .c { color: $comment }
+/* CommentHashbang */ .chroma .ch { color: $comment }
+/* CommentMultiline */ .chroma .cm { color: $comment }
+/* CommentSingle */ .chroma .c1 { color: $comment }
+/* CommentSpecial */ .chroma .cs { color: $comment }
+/* CommentPreproc */ .chroma .cp { color: $comment }
+/* CommentPreprocFile */ .chroma .cpf { color: $comment }
+/* GenericDeleted */ .chroma .gd { color: #ef6155 }
+/* GenericEmph */ .chroma .ge { font-style: italic }
+/* GenericHeading */ .chroma .gh { font-weight: bold }
+/* GenericInserted */ .chroma .gi { color: #48b685 }
+/* GenericPrompt */ .chroma .gp { color: $comment; font-weight: bold }
+/* GenericStrong */ .chroma .gs { font-weight: bold }
+/* GenericSubheading */ .chroma .gu { color: #5bc4bf; font-weight: bold }

+ 41 - 0
docs/assets/styles/_toc.scss

@@ -0,0 +1,41 @@
+@import "variables";
+
+aside.book-toc nav {
+    ul {
+        padding: 0;
+        margin: 0;
+        list-style: none;
+
+        li {
+            margin: 1em 0;
+        }
+
+        a {
+            display: block;
+        }
+
+        a:hover {
+            opacity: .5;
+        }
+    }
+    & > ul {
+        & > li:first-child {
+            & > a { display: none; }
+        }
+        & > li > a {
+            font-weight: bold;
+        }
+        & > li > ul {
+            padding-left: $padding-16;
+            & > li > ul {
+                padding-left: $padding-16;
+                li {
+                    // margin: 0.5em 0;
+                    a {
+                        color: lighten(desaturate($color-link, 70%), 15%);
+                    }
+                }
+            }
+        }
+    }
+}

+ 2 - 0
docs/assets/styles/_utils.scss

@@ -0,0 +1,2 @@
+@import "variables";
+

+ 56 - 0
docs/assets/styles/_variables.scss

@@ -0,0 +1,56 @@
+$padding-1: 1px;
+$padding-4: 0.25rem;
+$padding-8: 0.5rem;
+$padding-16: 1rem;
+
+$font-size-base: 16px;
+$font-size-12: 0.75rem;
+$font-size-14: 0.875rem;
+$font-size-16: 1rem;
+
+$brand-font-face: 'Lexend Deca', sans-serif;
+
+// Grayscale
+$white: #ffffff;
+$gray-100: #f8f9fa;
+$gray-200: #e9ecef;
+$gray-300: #dee2e6;
+$gray-400: #ced4da;
+$gray-500: #adb5bd;
+$gray-600: #868e96;
+$gray-700: #495057;
+$gray-800: #343a40;
+$gray-900: #212529;
+$black: #000;
+
+$color-link: #087298;
+$color-visited-link: #1e6198;
+$brand-color: #13b7f3;
+$color-default: #0079B8;
+$color-danger: #e12200;
+$color-warning: #FF8400;
+$color-success: #2F8400;
+$color-code-bg: $gray-800;
+$color-code-border: $gray-900;
+$color-code-text: $gray-200;
+
+$body-background: white;
+$body-font-color: $gray-800;
+$body-font-weight: normal;
+$body-min-width: 15rem;
+
+$top-bar-height: 50px;
+
+$nav-background: $body-background;
+$nav-link-color: $gray-800;
+
+$header-height: 3.5rem;
+$menu-width: 18rem;
+$toc-width: 14rem;
+
+$container-min-width: $body-min-width;
+$container-max-width: 80rem;
+
+$sm-breakpoint: $menu-width + $body-min-width;
+$md-breakpoint: $sm-breakpoint + $toc-width;
+$lg-breakpoint: 1200px;

+ 216 - 0
docs/assets/styles/main.scss

@@ -0,0 +1,216 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@import 'variables';
+@import 'markdown';
+@import 'utils';
+@import 'article';
+@import 'menu';
+@import 'shortcodes';
+@import 'syntax';
+@import 'search-widget';
+@import 'blog';
+@import 'toc';
+@import 'fonts';
+
+
+@layer components {
+  .btn-primary {
+    @apply bg-blue-500 border border-transparent rounded-md py-2 px-4 flex items-center justify-center text-base font-medium text-white font-medium hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-indigo-500;
+  }
+}
+
+
+[x-cloak] {
+  display: none !important;
+}
+
+
+ul.pagination {
+  display: flex;
+  justify-content: center;
+  list-style-type: none;
+
+  .page-item a {
+    padding: $padding-8 $padding-16;
+    border: 1px solid $gray-200;
+    border-radius: 3px;
+  }
+
+  .page-item.active a {
+    background-color: $gray-200;
+  }
+
+  .page-item.disabled a {
+    color: $gray-500;
+  }
+
+  li + li {
+    margin-left: $padding-4;
+  }
+}
+
+.container {
+  min-width: $container-min-width;
+  max-width: $container-max-width;
+  margin: $top-bar-height auto 0;
+}
+
+ul.contents-list {
+  padding: 0;
+  list-style-type: none;
+  text-transform: capitalize;
+}
+
+.book-brand {
+  margin-top: 0;
+}
+
+.book-page {
+  min-width: $body-min-width;
+  min-height: 80vh;
+  flex: 1;
+  padding: $padding-16 * 1.6;
+  padding-right: 3px;
+
+  @media all and (max-width: $sm-breakpoint) {
+    padding: 6px;
+  }
+
+  figure {
+    img {
+      max-width: 100%;
+    }
+  }
+}
+
+.book-header {
+  margin-bottom: $padding-16;
+  display: none;
+}
+
+
+.book-toc {
+  font-size: $font-size-12;
+
+  nav {
+    overflow-x: hidden;
+    overflow-y: auto;
+    max-height: 70vh;
+    padding-right: 1em;
+  }
+
+  nav > ul > li {
+    margin-bottom: 12px;
+  }
+
+  a.active {
+    //font-weight: bold;
+    text-decoration: underline;
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      left: -12px;
+      top: 8px;
+      display: block;
+      width: 6px;
+      height: 6px;
+      border-radius: 50%;
+      background-color: $brand-color;
+      opacity: 0.5;
+    }
+  }
+}
+
+.book-git-footer {
+  display: flex;
+  margin-top: $padding-16;
+  font-size: $font-size-14;
+  align-items: baseline;
+
+  img {
+    width: $font-size-14;
+    vertical-align: bottom;
+  }
+}
+
+.book-footer {
+  height: 200px;
+  text-align: center;
+  color: $gray-600;
+  margin-top: 60px;
+  font-size: 12px;
+}
+
+.book-posts {
+  min-width: $body-min-width;
+  max-width: $md-breakpoint;
+  padding: $padding-16;
+
+  article {
+    padding-bottom: $padding-16;
+  }
+}
+
+
+.texture-bg {
+  background-color: #1a1a1a;
+  box-shadow: inset 0px 0px 15px 8px rgba(0, 0, 0, 0.75);
+  background-image: url("/header-bg.png");
+  background-blend-mode: darken;
+}
+
+
+// Responsive styles
+aside nav,
+.book-page,
+.markdown {
+  transition: 0.2s ease-in-out;
+  transition-property: transform, margin-left, opacity;
+  will-change: transform, margin-left;
+}
+
+@media screen and (max-width: $md-breakpoint) {
+  .book-toc {
+    display: none;
+  }
+  .left-gutter {
+    display: none;
+  }
+}
+
+@media screen and (max-width: $sm-breakpoint) {
+  //.book-menu {
+  //    margin-left: -$menu-width;
+  //}
+
+  .book-header {
+    display: flex;
+  }
+
+  #menu-control:checked + main {
+    nav.book-menu,
+    .book-page {
+      transform: translateX($menu-width);
+    }
+
+    .book-header label {
+      transform: rotate(90deg);
+    }
+
+    .markdown {
+      opacity: 0.25;
+    }
+  }
+}
+
+#partner-levels-table {
+  tr:nth-child(even) {
+    th, td {
+      background-color: #f5f5f5;
+    }
+  }
+}

+ 22 - 0
docs/config.toml

@@ -0,0 +1,22 @@
+baseURL = "https://www.vendure.io/"
+languageCode = "en-us"
+title = "Vendure headless commerce"
+googleAnalytics = "UA-133911942-1"
+pygmentsCodeFences = true
+pygmentsUseClasses = true
+enableGitInfo = true
+images = ["share-image.jpg"]
+disableKinds = [
+  "taxonomy",
+  "taxonomyTerm"
+]
+[params]
+    description = "Vendure is an open-source headless commerce framework built on Node.js, TypeScript and GraphQL"
+    title = "Vendure: open-source headless commerce"
+[markup]
+  [markup.goldmark]
+    [markup.goldmark.renderer]
+      unsafe = true
+  [markup.tableOfContents]
+    endLevel = 3
+    startLevel = 1

+ 1 - 1
docs/content/deployment/deploying-admin-ui.md

@@ -5,7 +5,7 @@ showtoc: true
 
 ## Deploying the Admin UI
 
-If you have customized the Admin UI with extensions, you should [compile your extensions ahead of time as part of the deployment process]({{< relref "/docs/plugins/extending-the-admin-ui" >}}#compiling-as-a-deployment-step).
+If you have customized the Admin UI with extensions, you should [compile your extensions ahead of time as part of the deployment process]({{< relref "/plugins/extending-the-admin-ui" >}}#compiling-as-a-deployment-step).
 
 ### Deploying a stand-alone Admin UI
 

+ 5 - 6
docs/content/developer-guide/customizing-models.md

@@ -5,7 +5,7 @@ showtoc: true
  
 # Customizing Models with custom fields
 
-Custom fields allow you to add your own custom data properties to many of the Vendure entities. The entities which may have custom fields defined are listed in the [CustomFields documentation]({{< relref "/docs/typescript-api/custom-fields" >}})
+Custom fields allow you to add your own custom data properties to many of the Vendure entities. The entities which may have custom fields defined are listed in the [CustomFields documentation]({{< relref "/typescript-api/custom-fields" >}})
 
 They are specified in the VendureConfig:
 
@@ -29,7 +29,7 @@ With the example config above, the following will occur:
 
 1. The database schema will be altered, and a column will be added for each custom field. **Note: changes to custom fields require a database migration**. See the [Migrations guide]({{< relref "migrations" >}}).
 2. The GraphQL APIs will be modified to add the custom fields to the `Product` and `User` types respectively.
-3. If you are using the [admin-ui-plugin]({{< relref "/docs/typescript-api/admin-ui-plugin" >}}), the Admin UI detail pages will now contain form inputs to allow the custom field data to be added or edited.
+3. If you are using the [admin-ui-plugin]({{< relref "/typescript-api/admin-ui-plugin" >}}), the Admin UI detail pages will now contain form inputs to allow the custom field data to be added or edited.
 
 The values of the custom fields can then be set and queried via the GraphQL APIs:
 
@@ -71,8 +71,7 @@ The following types are available for custom fields:
 | `datetime`     | A datetime                   | date that variant is back in stock        |
 | `relation`     | A relation to another entity | Asset used as a customer avatar           |
 
-To see the underlying DB data type and GraphQL type used for each, see the [CustomFieldType doc]({{< relref "
-custom-field-type" >}}).
+To see the underlying DB data type and GraphQL type used for each, see the [CustomFieldType doc]({{< relref "custom-field-type" >}}).
 
 ## CustomField UI Form Inputs
 
@@ -139,7 +138,7 @@ OrderLine: [
 ]
 ```
 
-Once defined, the [addItemToOrder mutation]({{< relref "/docs/graphql-api/shop/mutations" >}}#additemtoorder) will have a third argument available, which accepts values for the custom field defined above:
+Once defined, the [addItemToOrder mutation]({{< relref "/graphql-api/shop/mutations" >}}#additemtoorder) will have a third argument available, which accepts values for the custom field defined above:
 
 ```GraphQL
 mutation {
@@ -194,7 +193,7 @@ export class EngravingPriceStrategy implements OrderItemPriceCalculationStrategy
 ## TypeScript Typings
 
 Because custom fields are generated at run-time, TypeScript has no way of knowing about them based on your
-VendureConfig. Consider the example above - let's say we have a [plugin]({{< relref "/docs/plugins" >}}) which needs to
+VendureConfig. Consider the example above - let's say we have a [plugin]({{< relref "/plugins" >}}) which needs to
 access the custom field values on a Product entity.
 
 Attempting to access the custom field will result in a TS compiler error:

+ 1 - 1
docs/content/developer-guide/error-handling.md

@@ -125,4 +125,4 @@ mutation ApplyCoupon($code: String!) {
 
 This ensures that your client code is aware of and handles all the usual error cases.
 
-You can see all the ErrorResult types returned by the Shop API mutations in the [Shop API Mutations docs]({{< relref "/docs/graphql-api/shop/mutations" >}}). 
+You can see all the ErrorResult types returned by the Shop API mutations in the [Shop API Mutations docs]({{< relref "/graphql-api/shop/mutations" >}}). 

+ 2 - 2
docs/content/developer-guide/importing-product-data.md

@@ -35,7 +35,7 @@ Here's an explanation of each column:
 * `name`: The name of the product. Rows with an empty "name" are interpreted as variants of the preceeding product row.
 * `slug`: The product's slug. Can be omitted, in which case will be generated from the name.
 * `description`: The product description.
-* `assets`: One or more asset file names separated by the pipe (`|`) character. The files can be located on the local file system, in which case the path is interpreted as being relative to the [`importAssetsDir`]({{< relref "/docs/typescript-api/import-export/import-export-options" >}}#importassetsdir) as defined in the VendureConfig. Files can also be urls which will be fetched from a remote http/https url. If you need more control over how assets are imported, you can implement a custom [AssetImportStrategy]({{< relref "asset-import-strategy" >}}). The first asset will be set as the featuredAsset.
+* `assets`: One or more asset file names separated by the pipe (`|`) character. The files can be located on the local file system, in which case the path is interpreted as being relative to the [`importAssetsDir`]({{< relref "/typescript-api/import-export/import-export-options" >}}#importassetsdir) as defined in the VendureConfig. Files can also be urls which will be fetched from a remote http/https url. If you need more control over how assets are imported, you can implement a custom [AssetImportStrategy]({{< relref "asset-import-strategy" >}}). The first asset will be set as the featuredAsset.
 * `facets`: One or more facets to apply to the product separated by the pipe (`|`) character. A facet has the format `<facet-name>:<facet-value>`.
 * `optionGroups`: OptionGroups define what variants make up the product. Applies only to products with more than one variant. 
 * `optionValues`: For each optionGroup defined, a corresponding value must be specified for each variant. Applies only to products with more than one variant.
@@ -84,7 +84,7 @@ Use of language codes has to be consistent throughout the file. You don't have t
 
 ## Initial Data
 
-As well as product data, other initialization data can be populated using the [`InitialData` object]({{< relref "initial-data" >}}). **This format is intentionally limited**; more advanced requirements (e.g. setting up ShippingMethods that use custom checkers & calculators) should be carried out via scripts which interact with the [Admin GraphQL API]({{< relref "/docs/graphql-api/admin" >}}).
+As well as product data, other initialization data can be populated using the [`InitialData` object]({{< relref "initial-data" >}}). **This format is intentionally limited**; more advanced requirements (e.g. setting up ShippingMethods that use custom checkers & calculators) should be carried out via scripts which interact with the [Admin GraphQL API]({{< relref "/graphql-api/admin" >}}).
 
 ```TypeScript
 import { InitialData, LanguageCode } from '@vendure/core';

+ 1 - 1
docs/content/developer-guide/job-queue/index.md

@@ -52,7 +52,7 @@ In this case it is recommended to try the [BullMQJobQueuePlugin]({{< relref "bul
 
 ## Using Job Queues in a plugin
 
-If you create a [Vendure plugin]({{< relref "/docs/plugins" >}}) which involves some long-running tasks, you can also make use of the job queue. See the [JobQueue plugin example]({{< relref "using-job-queue-service" >}}) for a detailed annotated example.
+If you create a [Vendure plugin]({{< relref "/plugins" >}}) which involves some long-running tasks, you can also make use of the job queue. See the [JobQueue plugin example]({{< relref "using-job-queue-service" >}}) for a detailed annotated example.
 
 {{< alert "primary" >}}
 A real example of this can be seen in the [EmailPlugin source](https://github.com/vendure-ecommerce/vendure/blob/master/packages/email-plugin/src/plugin.ts)

+ 2 - 2
docs/content/developer-guide/multi-tenant/index.md

@@ -63,7 +63,7 @@ For example, switching to the `ace-parts` Channel, and then creating a new Produ
 {{< alert "warning" >}}
 **Note:** Care must be taken if you log in with the superadmin account in the default Channel, especially with regard to prices and currencies.
 
-See more details see the Channels guide on [Channels, currencies & prices]({{< relref "/docs/developer-guide/channels" >}}#channels-currencies--prices), and in particular the [multiple shops use-cases]({{< relref "/docs/developer-guide/channels" >}}#use-case-multiple-separate-shops)
+See more details see the Channels guide on [Channels, currencies & prices]({{< relref "/developer-guide/channels" >}}#channels-currencies--prices), and in particular the [multiple shops use-cases]({{< relref "/developer-guide/channels" >}}#use-case-multiple-separate-shops)
 {{< /alert >}}
 
 ## The Storefront
@@ -76,7 +76,7 @@ https://my-vendure-server.com/shop-api?vendure-token=best-choice
 
 ## Determining the Active Channel
 
-When developing plugins and writing custom server code in general, you'll often want to know the Channel that the current request is using. This can be determined from the [RequestContext.channel]({{< relref "/docs/typescript-api/request/request-context" >}}#channel) property.
+When developing plugins and writing custom server code in general, you'll often want to know the Channel that the current request is using. This can be determined from the [RequestContext.channel]({{< relref "/typescript-api/request/request-context" >}}#channel) property.
 
 ```TypeScript
 createPayment: async (ctx, order, amount, args) => {

+ 4 - 4
docs/content/developer-guide/overview/_index.md

@@ -13,7 +13,7 @@ Here is a simplified diagram of the Vendure application architecture:
 
 ## Entry Points
 
-As you can see in the diagram, there are two entry points into the application: [`bootstrap()`]({{< relref "bootstrap" >}}) and [`bootstrapWorker()`]({{< relref "bootstrap-worker" >}}), which start the main server and the [worker]({{< relref "/docs/developer-guide/vendure-worker" >}}) respectively. Communication between server and worker(s) is done via the [Job Queue]({{< relref "/docs/developer-guide/job-queue" >}}).
+As you can see in the diagram, there are two entry points into the application: [`bootstrap()`]({{< relref "bootstrap" >}}) and [`bootstrapWorker()`]({{< relref "bootstrap-worker" >}}), which start the main server and the [worker]({{< relref "/developer-guide/vendure-worker" >}}) respectively. Communication between server and worker(s) is done via the [Job Queue]({{< relref "/developer-guide/job-queue" >}}).
 
 ## GraphQL APIs
 
@@ -21,10 +21,10 @@ There are 2 separate GraphQL APIs: shop and admin.
 
 * The **Shop API** is used by public-facing client applications (web shops, e-commerce apps, mobile apps etc.) to allow customers to find products and place orders. 
     
-    [Shop API Documentation]({{< relref "/docs/graphql-api/shop" >}}).
+    [Shop API Documentation]({{< relref "/graphql-api/shop" >}}).
 * The **Admin API** is used by administrators to manage products, customers and orders. 
 
-    [Admin API Documentation]({{< relref "/docs/graphql-api/admin" >}}).
+    [Admin API Documentation]({{< relref "/graphql-api/admin" >}}).
 
 ## Database
 
@@ -32,4 +32,4 @@ Vendure officially supports multiple databases: MySQL/MariaDB, PostgreSQL, SQLit
 
 ## Custom Business Logic (Plugins)
 
-Not shown on the diagram (for the sake of simplicity) are plugins. Plugins are the mechanism by which you extend Vendure with your own business logic and functionality. See [the Plugins docs]({{< relref "/docs/plugins" >}})
+Not shown on the diagram (for the sake of simplicity) are plugins. Plugins are the mechanism by which you extend Vendure with your own business logic and functionality. See [the Plugins docs]({{< relref "/plugins" >}})

+ 2 - 2
docs/content/developer-guide/payment-integrations/index.md

@@ -119,7 +119,7 @@ Once the PaymentMethodHandler is defined as above, you can use it to create a ne
 
 ## Payment flow
 
-1. Once the active Order has been transitioned to the ArrangingPayment state (see the [Order Workflow guide]({{< relref "order-workflow" >}})), one or more Payments are created by executing the [`addPaymentToOrder` mutation]({{< relref "/docs/graphql-api/shop/mutations#addpaymenttoorder" >}}). This mutation has a required `method` input field, which _must_ match the `code` of one of the configured PaymentMethodHandlers. In the case above, this would be set to `"my-payment-method"`.
+1. Once the active Order has been transitioned to the ArrangingPayment state (see the [Order Workflow guide]({{< relref "order-workflow" >}})), one or more Payments are created by executing the [`addPaymentToOrder` mutation]({{< relref "/graphql-api/shop/mutations#addpaymenttoorder" >}}). This mutation has a required `method` input field, which _must_ match the `code` of one of the configured PaymentMethodHandlers. In the case above, this would be set to `"my-payment-method"`.
     ```GraphQL
     mutation {
         addPaymentToOrder(input: {
@@ -130,7 +130,7 @@ Once the PaymentMethodHandler is defined as above, you can use it to create a ne
     }
     ```
     The `metadata` field is used to store the specific data required by the payment provider. E.g. some providers have a client-side part which begins the transaction and returns a token which must then be verified on the server side.
-2. This mutation internally invokes the [PaymentMethodHandler's `createPayment()` function]({{< relref "payment-method-config-options" >}}#createpayment). This function returns a [CreatePaymentResult object]({{< relref "payment-method-types" >}}#payment-method-types) which is used to create a new [Payment]({{< relref "/docs/typescript-api/entities/payment" >}}). If the Payment amount equals the order total, then the Order is transitioned to either the "PaymentAuthorized" or "PaymentSettled" state and the customer checkout flow is complete.
+2. This mutation internally invokes the [PaymentMethodHandler's `createPayment()` function]({{< relref "payment-method-config-options" >}}#createpayment). This function returns a [CreatePaymentResult object]({{< relref "payment-method-types" >}}#payment-method-types) which is used to create a new [Payment]({{< relref "/typescript-api/entities/payment" >}}). If the Payment amount equals the order total, then the Order is transitioned to either the "PaymentAuthorized" or "PaymentSettled" state and the customer checkout flow is complete.
 
 ### Single-step
 

+ 1 - 1
docs/content/developer-guide/promotions.md

@@ -17,7 +17,7 @@ Promotions are a means of offering discounts on an order based on various criter
 All Promotions can have the following constraints applied to them:
 
 -   **Date range** Using the "starts at" and "ends at" fields, the Promotion can be scheduled to only be active during the given date range.
--   **Coupon code** A Promotion can require a coupon code first be activated using the [`applyCouponCode` mutation]({{< relref "/docs/graphql-api/shop/mutations" >}}#applycouponcode) in the Shop API.
+-   **Coupon code** A Promotion can require a coupon code first be activated using the [`applyCouponCode` mutation]({{< relref "/graphql-api/shop/mutations" >}}#applycouponcode) in the Shop API.
 -   **Per-customer limit** A Promotion coupon may be limited to a given number of uses per Customer.
 
 ### Conditions

+ 1 - 1
docs/content/developer-guide/shipping.md

@@ -4,7 +4,7 @@ showtoc: true
 ---
 # Shipping & Fulfillment
 
-Shipping in Vendure is handled by [ShippingMethods]({{< relref "shipping-method" >}}). Multiple ShippingMethods can be set up and then your storefront can query [`eligibleShippingMethods`]({{< relref "/docs/graphql-api/shop/queries" >}}#eligibleshippingmethods) to find out which ones can be applied to the active order.
+Shipping in Vendure is handled by [ShippingMethods]({{< relref "shipping-method" >}}). Multiple ShippingMethods can be set up and then your storefront can query [`eligibleShippingMethods`]({{< relref "/graphql-api/shop/queries" >}}#eligibleshippingmethods) to find out which ones can be applied to the active order.
 
 A ShippingMethod is composed of a **checker** and a **calculator**. When querying `eligibleShippingMethods`, each of the defined ShippingMethods' checker functions is executed to find out whether the order is eligible for that method, and if so, the calculator is executed to determine the shipping cost.
 

+ 1 - 1
docs/content/developer-guide/stock-control/index.md

@@ -32,7 +32,7 @@ Stock on hand | Allocated | Out-of-stock threshold | Saleable
 10            | 10        | 0                      | 0
 10            | 10        | -5                     | 5
 
-The saleable value is what determines whether the customer is able to add a variant to an order. If there is 0 saleable stock, then any attempt to add to the order will result in an [`InsufficientStockError`]({{< relref "/docs/graphql-api/shop/object-types" >}}#insufficientstockerror).
+The saleable value is what determines whether the customer is able to add a variant to an order. If there is 0 saleable stock, then any attempt to add to the order will result in an [`InsufficientStockError`]({{< relref "/graphql-api/shop/object-types" >}}#insufficientstockerror).
 
 ```JSON
 {

+ 1 - 1
docs/content/developer-guide/taxes.md

@@ -44,7 +44,7 @@ When a customer adds an item to the Order, the following logic takes place:
 1. The price of the OrderItem, and whether or not that price is inclusive of tax, is determined according to the configured [OrderItemPriceCalculationStrategy]({{< relref "order-item-price-calculation-strategy" >}}).
 2. The active tax Zone is determined based on the configured [TaxZoneStrategy]({{< relref "tax-zone-strategy" >}}).
 3. The applicable TaxRate is fetched based on the ProductVariant's TaxCategory and the active tax Zone determined in step 1.
-4. The `TaxLineCalculationStrategy.calculate()` of the configured [TaxLineCalculationStrategy]({{< relref "tax-line-calculation-strategy" >}}) is called, which will return one or more [TaxLines]({{< relref "/docs/graphql-api/shop/object-types" >}}#taxline).
+4. The `TaxLineCalculationStrategy.calculate()` of the configured [TaxLineCalculationStrategy]({{< relref "tax-line-calculation-strategy" >}}) is called, which will return one or more [TaxLines]({{< relref "/graphql-api/shop/object-types" >}}#taxline).
 5. The final `priceWithTax` of the OrderItem is calculated based on all the above.
 
 ## Calculating taxes on shipping

+ 2 - 2
docs/content/developer-guide/uploading-files.md

@@ -5,7 +5,7 @@ showtoc: true
 
 # Uploading Files 
 
-Vendure handles file uploads with the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). Internally, we use the [graphql-upload package](https://github.com/jaydenseric/graphql-upload). Once uploaded, a file is known as an [Asset]({{< relref "/docs/typescript-api/entities/asset" >}}). Assets are typically used for images, but can represent any kind of binary data such as PDF files or videos.
+Vendure handles file uploads with the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec). Internally, we use the [graphql-upload package](https://github.com/jaydenseric/graphql-upload). Once uploaded, a file is known as an [Asset]({{< relref "/typescript-api/entities/asset" >}}). Assets are typically used for images, but can represent any kind of binary data such as PDF files or videos.
 
 ## Upload clients
 
@@ -15,7 +15,7 @@ For testing, it is even possible to use a [plain curl request](https://github.co
 
 ## The `createAssets` mutation
 
-The [createAssets mutation]({{< relref "/docs/graphql-api/admin/mutations" >}}#createassets) in the Admin API is the only means of uploading files by default. 
+The [createAssets mutation]({{< relref "/graphql-api/admin/mutations" >}}#createassets) in the Admin API is the only means of uploading files by default. 
 
 Here's an example of how a file upload would look using the `apollo-upload-client` package:
 

+ 1 - 1
docs/content/developer-guide/vendure-worker.md

@@ -70,7 +70,7 @@ bootstrap(config)
 
 ## Running custom code on the worker
 
-If you are authoring a [Vendure plugin]({{< relref "/docs/plugins" >}}) to implement custom functionality, you can also make use of the worker process in order to handle long-running or computationally-demanding tasks. See the [Plugin Examples]({{< relref "plugin-examples" >}}#running-processes-on-the-worker) page for an example of this.
+If you are authoring a [Vendure plugin]({{< relref "/plugins" >}}) to implement custom functionality, you can also make use of the worker process in order to handle long-running or computationally-demanding tasks. See the [Plugin Examples]({{< relref "plugin-examples" >}}#running-processes-on-the-worker) page for an example of this.
 
 ## ProcessContext
 

+ 3 - 3
docs/content/getting-started.md

@@ -73,6 +73,6 @@ Log in with the superadmin credentials:
 
 ## Next Steps
 
-* Learn more about [Configuration]({{< relref "/docs/developer-guide/configuration" >}}) of your Vendure server.
-* Get a better understanding of how Vendure works by reading the [Architecture Overview]({{< relref "/docs/developer-guide/overview" >}})
-* Learn how to implement a storefront with the [GraphQL API Guide]({{< relref "/docs/storefront/shop-api-guide" >}})
+* Learn more about [Configuration]({{< relref "/developer-guide/configuration" >}}) of your Vendure server.
+* Get a better understanding of how Vendure works by reading the [Architecture Overview]({{< relref "/developer-guide/overview" >}})
+* Learn how to implement a storefront with the [GraphQL API Guide]({{< relref "/storefront/shop-api-guide" >}})

+ 0 - 13
docs/content/graphql-api/admin/_index.md

@@ -1,13 +0,0 @@
----
-title: "Admin API"
-weight: 4
-showtoc: false
----
-
-# GraphQL Admin API
-
-The Admin API is primarily used by the included Admin UI web app to perform administrative tasks such as inventory management, order tracking etc.
-
-{{% alert %}}
-Explore the interactive GraphQL Admin API at [demo.vendure.io/admin-api](https://demo.vendure.io/admin-api)
-{{< /alert >}}

+ 0 - 13
docs/content/graphql-api/shop/_index.md

@@ -1,13 +0,0 @@
----
-title: "Shop API"
-weight: 3
-showtoc: false
----
-
-# GraphQL Shop API
-
-The Shop API is used by storefront applications. It provides all the necessary queries and mutations for finding and viewing products, creating and updating orders, checking out, managing a customer account etc.
-
-{{% alert %}}
-Explore the interactive GraphQL Shop API at [demo.vendure.io/shop-api](https://demo.vendure.io/shop-api)
-{{< /alert >}}

+ 1 - 1
docs/content/plugins/_index.md

@@ -7,7 +7,7 @@ weight: 1
 
 Plugins are the method by which the built-in functionality of Vendure can be extended. Plugins in Vendure allow one to:
 
-* Modify the [VendureConfig]({{< ref "/docs/typescript-api/configuration" >}}#vendureconfig) object.
+* Modify the [VendureConfig]({{< ref "/typescript-api/configuration" >}}#vendureconfig) object.
 * Extend the GraphQL API, including modifying existing types and adding completely new queries and mutations.
 * Define new database entities and interact directly with the database.
 * Run code before the server bootstraps, such as starting web servers.

+ 1 - 1
docs/content/plugins/extending-the-admin-ui/custom-form-inputs/_index.md

@@ -5,7 +5,7 @@ weight: 5
 
 # Custom Form Inputs
 
-Another way to extend the Admin UI app is to define custom form input components for manipulating any [Custom Fields]({{< ref "/docs/typescript-api/custom-fields" >}}) you have defined on your entities as well as [configurable args]({{< relref "config-args" >}}) used by custom [ConfigurableOperationDefs]({{< relref "configurable-operation-def" >}}).
+Another way to extend the Admin UI app is to define custom form input components for manipulating any [Custom Fields]({{< ref "/typescript-api/custom-fields" >}}) you have defined on your entities as well as [configurable args]({{< relref "config-args" >}}) used by custom [ConfigurableOperationDefs]({{< relref "configurable-operation-def" >}}).
 
 ## For Custom Fields
 

+ 1 - 1
docs/content/plugins/plugin-architecture/_index.md

@@ -10,7 +10,7 @@ showtoc: true
 
 A plugin in Vendure is a specialized Nestjs Module that is decorated with the [`VendurePlugin` class decorator]({{< relref "vendure-plugin" >}}). This diagram illustrates how a plugin can integrate with and extend Vendure.
  
-1. A Plugin may define logic to be run by the [Vendure Worker]({{< relref "/docs/developer-guide/vendure-worker" >}}). This is suitable for long-running or resource-intensive tasks.
+1. A Plugin may define logic to be run by the [Vendure Worker]({{< relref "/developer-guide/vendure-worker" >}}). This is suitable for long-running or resource-intensive tasks.
 2. A Plugin can modify any aspect of server configuration via the [`configuration` metadata property]({{< relref "vendure-plugin-metadata" >}}#configuration).
 3. A Plugin can extend the GraphQL APIs via the [`shopApiExtensions` metadata property]({{< relref "vendure-plugin-metadata" >}}#shopapiextensions) and the [`adminApiExtensions` metadata property]({{< relref "vendure-plugin-metadata" >}}#adminapiextensions).
 4. A Plugin can interact with Vendure by importing the [`PluginCommonModule`]({{< relref "plugin-common-module" >}}), by which it may inject any of the core Vendure services (which are responsible for all interaction with the database as well as business logic). Additionally, a plugin may define new database entities via the [`entities` metadata property]({{< relref "vendure-plugin-metadata" >}}#entities) and otherwise define any other providers and controllers just like any [Nestjs module](https://docs.nestjs.com/modules).

+ 1 - 1
docs/content/plugins/plugin-examples/using-job-queue-service.md

@@ -6,7 +6,7 @@ showtoc: true
 
 # Using the Job Queue
 
-If your plugin involves long-running tasks, you can take advantage of the [job queue system]({{< relref "/docs/developer-guide/job-queue" >}}) that comes with Vendure. This example defines a mutation that can be used to transcode and link a video to a Product's customFields.
+If your plugin involves long-running tasks, you can take advantage of the [job queue system]({{< relref "/developer-guide/job-queue" >}}) that comes with Vendure. This example defines a mutation that can be used to transcode and link a video to a Product's customFields.
 
 ```TypeScript
 // product-video.resolver.ts

+ 1 - 1
docs/content/plugins/writing-a-vendure-plugin.md

@@ -121,7 +121,7 @@ Some explanations of this code are in order:
 * We are able to use Nest's dependency injection to inject an instance of our `CatFetcher` class into the constructor of the resolver. We are also injecting an instance of the built-in [ProductService class]({{< relref "product-service" >}}), which is responsible for operations on Products.
 * We use the `@Transaction()` decorator to ensure that all database operations in this resolver are run within a transaction. This ensures that if any part of it fails, all changes will be rolled back, keeping our data in a consistent state. For more on this, see the [Transaction Decorator docs]({{< relref "transaction-decorator" >}}).
 * We use the `@Mutation()` decorator to mark this method as a resolver for the GraphQL mutation with the corresponding name.
-* The `@Allow()` decorator enables us to define permissions restrictions on the mutation. Only those users whose permissions include `UpdateCatalog` may perform this operation. For a full list of available permissions, see the [Permission enum]({{< relref "/docs/graphql-api/admin/enums" >}}#permission). Plugins may also define custom permissions, see [Defining custom permissions]({{< relref "defining-custom-permissions" >}}).
+* The `@Allow()` decorator enables us to define permissions restrictions on the mutation. Only those users whose permissions include `UpdateCatalog` may perform this operation. For a full list of available permissions, see the [Permission enum]({{< relref "/graphql-api/admin/enums" >}}#permission). Plugins may also define custom permissions, see [Defining custom permissions]({{< relref "defining-custom-permissions" >}}).
 * The `@Ctx()` decorator injects the current [RequestContext]({{< relref "request-context" >}}) into the resolver. This provides information about the current request such as the current Session, User and Channel. It is required by most of the internal service methods.
 * The `@Args()` decorator injects the arguments passed to the mutation as an object.
 

+ 12 - 0
docs/content/searchindex/_index.md

@@ -0,0 +1,12 @@
+---
+title: "Search Index"
+layout: "searchindex"
+hideFromMenu: true
+---
+
+# search index
+
+The content of this file is not actually used - it is just a placeholder so that Hugo will generate a file named `searchindex/`.
+
+The contents of this file are actually generated by the [searchindex.html](../../layouts/searchindex/searchindex.html) template, which loops over all the content and extracts the title, heading and url data into a JSON data structure which can be loaded async by the [SearchWidget class](../../assets/scripts/search-widget.ts).
+

+ 1 - 1
docs/content/storefront/building-a-storefront/_index.md

@@ -12,7 +12,7 @@ One of the benefits of Vendure's headless architecture is that you can build you
 
 ## Storefront starters
 
-To get you up and running with your storefront implementation, we offer a number of integrations with popular front-end frameworks such as Remix, Angular & Qwik. See all of our [storefront integrations]({{< relref "integration" >}}).
+To get you up and running with your storefront implementation, we offer a number of integrations with popular front-end frameworks such as Remix, Angular & Qwik. See all of our [storefront integrations](https://demo.vendure.io/).
 
 ## Custom-building
 

+ 2 - 2
docs/content/storefront/order-workflow/_index.md

@@ -5,7 +5,7 @@ showtoc: true
 
 # Order Workflow
 
-An Order is a collection of one or more ProductVariants which can be purchased by a Customer. Orders are represented internally by the [Order entity]({{< relref "order" >}}) and in the GraphQL API by the [Order type]({{< relref "/docs/graphql-api/shop/object-types#order" >}}).
+An Order is a collection of one or more ProductVariants which can be purchased by a Customer. Orders are represented internally by the [Order entity]({{< relref "order" >}}) and in the GraphQL API by the [Order type]({{< relref "/graphql-api/shop/object-types#order" >}}).
 
 ## Order State
 
@@ -29,7 +29,7 @@ So if the customer adds 2 *Widgets* to the Order, there will be **one OrderLine*
 
 ## Shop client order workflow
 
-The [GraphQL Shop API Guide]({{< relref "/docs/storefront/shop-api-guide" >}}#order-flow) lists the GraphQL operations you will need to implement this workflow in your storefront client application.
+The [GraphQL Shop API Guide]({{< relref "/storefront/shop-api-guide" >}}#order-flow) lists the GraphQL operations you will need to implement this workflow in your storefront client application.
 
 In this section, we'll cover some examples of how these operations would look in your storefront.
 

+ 3 - 3
docs/content/storefront/shop-api-guide.md

@@ -9,7 +9,7 @@ showtoc: true
 This is an overview of the GraphQL Shop API, which is used when implementing a storefront application with Vendure. 
 
 {{< alert "warning" >}}
-This guide only lists some of the more common operations you'll need for your storefront. Please consult [the Shop API reference]({{< relref "/docs/graphql-api/shop" >}}) for a complete guide.
+This guide only lists some of the more common operations you'll need for your storefront. Please consult [the Shop API reference]({{< relref "/graphql-api/shop" >}}) for a complete guide.
 {{< /alert >}}
 
 ## Universal Parameters
@@ -21,7 +21,7 @@ There are a couple of query parameters that are valid for all GraphQL operations
   ```text
   POST http://localhost:3000/shop-api?languageCode=de
   ```
-* `vendure-token`: If your Vendure instance features more than a single [Channel]({{< relref "/docs/developer-guide/channels" >}}), the token of the active Channel can be specified by token as either a query parameter _or_ as a header. The name of the key can be configured by the [`channelTokenKey` config option]({{< relref "vendure-config" >}}#channeltokenkey).
+* `vendure-token`: If your Vendure instance features more than a single [Channel]({{< relref "/developer-guide/channels" >}}), the token of the active Channel can be specified by token as either a query parameter _or_ as a header. The name of the key can be configured by the [`channelTokenKey` config option]({{< relref "vendure-config" >}}#channeltokenkey).
 
 ## Browsing the catalogue
 
@@ -111,7 +111,7 @@ Use the `product` query for the Product detail view.
 * {{< shop-api-operation operation="registerCustomerAccount" type="mutation" >}}: Creates a new Customer account.
 * {{< shop-api-operation operation="login" type="mutation" >}}: Log in with registered Customer credentials.
 * {{< shop-api-operation operation="logout" type="mutation" >}}: Log out from Customer account.
-* {{< shop-api-operation operation="activeCustomer" type="query" >}}: Returns the current logged-in Customer, or `null` if not logged in. This is useful for displaying the logged-in status in the storefront. The returned [`Customer`]({{< relref "/docs/graphql-api/shop/object-types" >}}#customer) type can also be used to query the Customer's Order history, list of Addresses and other personal details.
+* {{< shop-api-operation operation="activeCustomer" type="query" >}}: Returns the current logged-in Customer, or `null` if not logged in. This is useful for displaying the logged-in status in the storefront. The returned [`Customer`]({{< relref "/graphql-api/shop/object-types" >}}#customer) type can also be used to query the Customer's Order history, list of Addresses and other personal details.
 * {{< shop-api-operation operation="requestPasswordReset" type="mutation" >}}: Use this to implement a "forgotten password" flow. This will trigger a password reset email to be sent.
 * {{< shop-api-operation operation="resetPassword" type="mutation" >}}: Use the token provided in the password reset email to set a new password.
 

+ 2 - 2
docs/content/user-guide/catalog/products.md

@@ -21,7 +21,7 @@ In the diagram above you'll notice that it is the ProductVariants which have an
 
 ## Tracking Inventory
 
-Vendure can track the stock levels of each of your ProductVariants. This is done by setting the "track inventory" option to "track" (or "inherit from global settings" if the [global setting]({{< relref "/docs/user-guide/settings/global-settings" >}}) is set to track).
+Vendure can track the stock levels of each of your ProductVariants. This is done by setting the "track inventory" option to "track" (or "inherit from global settings" if the [global setting]({{< relref "/user-guide/settings/global-settings" >}}) is set to track).
 
 {{< figure src="../screen-inventory.webp" >}}
 
@@ -33,4 +33,4 @@ When tracking inventory:
 
 ### Back orders
 
-Back orders can be enabled by setting a **negative value** as the "Out-of-stock threshold". This can be done via [global settings]({{< relref "/docs/user-guide/settings/global-settings" >}}) or on a per-variant basis.
+Back orders can be enabled by setting a **negative value** as the "Out-of-stock threshold". This can be done via [global settings]({{< relref "/user-guide/settings/global-settings" >}}) or on a per-variant basis.

+ 1 - 1
docs/content/user-guide/localization/index.md

@@ -29,7 +29,7 @@ Vendure supports **admin-facing** (Admin API and Admin UI) localization by allow
 
 ## How to enable languages
 
-To select the set of languages you wish to create translations for, set them in the [global settings]({{< relref "/docs/user-guide/settings/global-settings" >}}).
+To select the set of languages you wish to create translations for, set them in the [global settings]({{< relref "/user-guide/settings/global-settings" >}}).
 
 Once more than one language is enabled, you will see a language switcher appear when editing the object types listed above.
 

+ 2 - 2
docs/content/user-guide/promotions/index.md

@@ -16,8 +16,8 @@ A condition defines the criteria that must be met for the Promotion to be activa
 
 * If the order total is at least $X
 * Buy at least X of a certain product
-* But at least X of any product with the specified [FacetValues]({{< relref "/docs/user-guide/catalog/facets" >}})
-* If the customer is a member of the specified [Customer Group]({{< relref "/docs/user-guide/customers" >}}#customer-groups)
+* But at least X of any product with the specified [FacetValues]({{< relref "/user-guide/catalog/facets" >}})
+* If the customer is a member of the specified [Customer Group]({{< relref "/user-guide/customers" >}}#customer-groups)
 
 Vendure allows completely custom conditions to be defined by your developers, implementing the specific logic needed by your business.
 

+ 1 - 1
docs/content/user-guide/settings/countries-zones.md

@@ -8,5 +8,5 @@ title: "Countries & Zones"
 
 By default, Vendure includes all countries in the list, but you are free to remove or disable any that you don't need.
 
-**Zones** provide a way to group countries. Zones are used mainly for defining [tax rates]({{< relref "/docs/user-guide/settings/taxes" >}}) and can also be used in shipping calculations.
+**Zones** provide a way to group countries. Zones are used mainly for defining [tax rates]({{< relref "/user-guide/settings/taxes" >}}) and can also be used in shipping calculations.
 

+ 2 - 2
docs/content/user-guide/settings/global-settings.md

@@ -8,5 +8,5 @@ The global settings allow you to define certain configurations that affect _all_
 
 * **Available languages** defines which languages you wish to make available for translations. When more than one language has been enabled, you will see the language switcher appear when viewing translatable objects such as products, collections, facets and shipping methods.
   {{< figure src="../screen-translations.webp" >}}
-* **Global out-of-stock threshold** sets the stock level at which a product variant is considered to be out of stock. Using a negative value enables backorder support. This setting can be overridden by individual product variants (see the [tracking inventory]({{< relref "/docs/user-guide/catalog/products" >}}#tracking-inventory) guide).
-* **Track inventory by default** sets whether stock levels should be tracked. This setting can be overridden by individual product variants (see the [tracking inventory]({{< relref "/docs/user-guide/catalog/products" >}}#tracking-inventory) guide).
+* **Global out-of-stock threshold** sets the stock level at which a product variant is considered to be out of stock. Using a negative value enables backorder support. This setting can be overridden by individual product variants (see the [tracking inventory]({{< relref "/user-guide/catalog/products" >}}#tracking-inventory) guide).
+* **Track inventory by default** sets whether stock levels should be tracked. This setting can be overridden by individual product variants (see the [tracking inventory]({{< relref "/user-guide/catalog/products" >}}#tracking-inventory) guide).

+ 1 - 1
docs/content/user-guide/settings/taxes.md

@@ -29,4 +29,4 @@ Tax rates set the rate of tax for a given **tax category** destined for a partic
 
 ## Tax Compliance
 
-Please note that tax compliance is a complex topic that varies significantly between countries. Vendure does not (and cannot) offer a complete out-of-the-box tax solution which is guaranteed to be compliant with your use-case. What we strive to do is to provide a very flexible set of tools that your developers can use to tailor tax calculations exactly to your needs. These are covered in the [Developer's guide to taxes]({{< relref "/docs/developer-guide/taxes" >}}). 
+Please note that tax compliance is a complex topic that varies significantly between countries. Vendure does not (and cannot) offer a complete out-of-the-box tax solution which is guaranteed to be compliant with your use-case. What we strive to do is to provide a very flexible set of tools that your developers can use to tailor tax calculations exactly to your needs. These are covered in the [Developer's guide to taxes]({{< relref "/developer-guide/taxes" >}}). 

+ 17 - 0
docs/layouts/404.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+{{- partial "docs/shared" -}}
+<html>
+
+<head>
+  {{ partial "docs/html-head" . }}
+</head>
+
+<body>
+  <main class="flex justify-center">
+      <h1>404 Not Found</h1>
+  </main>
+
+  {{ template "_internal/google_analytics_async.html" . }}
+</body>
+
+</html>

+ 117 - 0
docs/layouts/_default/baseof.html

@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+{{- partial "docs/shared" -}}
+<html>
+
+<head>
+    {{ partial "docs/html-head" . }}
+    {{ template "_internal/opengraph.html" . }}
+    {{ template "_internal/twitter_cards.html" . }}
+</head>
+
+<body>
+
+{{ partial "top-bar" (dict "ctx" . "isDocsPage" true "isLandingPage" false) }}
+<main class="mx-auto lg:max-w-screen-xl lg:grid lg:grid-cols-12 lg:gap-8">
+
+    <aside class="lg:relative lg:col-span-3" x-data="Components.popover(true)" x-init="open = window.innerWidth > 1024">
+        <div class="sticky top-0 lg:w-72 flex flex-col lg:h-screen">
+            <div class="flex justify-center items-center px-1 py-2 bg-gray-100">
+                <label for="search" class="sr-only">Search docs (ctrl + k)</label>
+                <input type="text" name="search" id="searchInputTrigger"
+                       class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
+                       placeholder="Search docs (ctrl+k)">
+                <button class="p-2 ml-2 lg:hidden" @click="toggle">
+                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
+                      <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
+                    </svg>
+                </button>
+            </div>
+            <nav x-show="open"
+                 x-transition:enter="transition ease-out duration-200"
+                 x-transition:enter-start="opacity-0"
+                 x-transition:enter-end="opacity-100"
+                 x-transition:leave="duration-100 ease-in"
+                 x-transition:leave-start="opacity-100"
+                 x-transition:leave-end="opacity-0"
+                 class="book-menu text-sm flex-1 bg-gray-100 overflow-y-auto overflow-x-hidden flex flex-col" role="navigation">
+                <div class="pl-6 flex-1">
+                    {{ if .Site.Params.BookMenuBundle }}
+                    {{ partial "docs/menu-bundle" . }}
+                    {{ else }}
+                    {{ partial "menu-filetree" . }}
+                    {{ end }}
+                </div>
+                <div class="text-sm w-full border-t border-dotted border-gray-200 py-4 text-gray-400 text-center lowercase">
+                    <a href="https://github.com/vendure-ecommerce/vendure/releases/tag/v{{ $.Site.Data.build.version }}">
+                        v{{ $.Site.Data.build.version }}</a>#<a
+                        href="https://github.com/vendure-ecommerce/vendure/commit/{{ $.Site.Data.build.commit }}">{{
+                    $.Site.Data.build.commit }}
+                </a>
+                </div>
+            </nav>
+        </div>
+    </aside>
+
+    <div class="lg:col-span-7 mx-2">
+        {{ template "main" . }}
+
+        <div class="book-footer">
+            {{ if gt (dateFormat "2006" $.Page.Lastmod) 2018 }}
+            Generated on {{ dateFormat "Jan 2 2006 at 15:04" $.Page.Lastmod }}
+            {{ end }}
+        </div>
+    </div>
+
+    <div class="lg:col-span-2">
+        <div class="hidden lg:block sticky top-0 mt-2 py-1 pl-4 rounded">
+            {{ if ge (len .TableOfContents) 150 }}
+                <div class="uppercase text-blue-500 tracking-wider text-sm mt-2">Contents</div>
+                {{ template "toc" . }}
+            {{ end }}
+        </div>
+    </div>
+</main>
+
+<div x-data="Components.popover(false)" @click.self="open = true" id="searchOverlay" @keydown.escape.window="open = false">
+    <div x-show="open" x-cloak class="fixed z-10 inset-0 overflow-y-auto" aria-labelledby="modal-title" role="dialog"
+         aria-modal="true">
+        <div class="items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:p-0">
+            <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
+                 aria-hidden="true"
+                 x-show="open"
+                 x-transition:enter="ease-out duration-300"
+                 x-transition:enter-start="opacity-0"
+                 x-transition:enter-end="opacity-100"
+                 x-transition:leave="ease-in duration-200"
+                 x-transition:leave-start="opacity-100"
+                 x-transition:leave-end="opacity-0"
+            ></div>
+
+            <div class="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full sm:p-6"
+                 x-show="open"
+                 x-transition:enter="ease-out duration-300"
+                 x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
+                 x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
+                 x-transition:leave="ease-in duration-200"
+                 x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
+                 x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
+            >
+                <div class="relative">
+                    <input type="text" name="search" id="searchInput"
+                           class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
+                           placeholder="Search docs">
+                    <button class="uppercase text-sm font-bold fixed rounded border border-gray-200 text-gray-400 px-2 right-8 top-7"
+                            @click="open = false">
+                        esc
+                    </button>
+                </div>
+                <div id="autocomplete-wrapper" class="mt-4"></div>
+            </div>
+        </div>
+    </div>
+</div>
+
+{{ template "_internal/google_analytics_async.html" . }}
+</body>
+
+</html>

+ 21 - 0
docs/layouts/_default/list.html

@@ -0,0 +1,21 @@
+{{ define "main" }}
+<article class="markdown">
+    {{ partial "breadcrumbs" . }}
+    {{- .Content -}}
+
+    {{ if gt (len .Pages) 0 }}
+    <h3>Contents:</h3>
+    <ul class="contents-list">
+        {{ range .Pages.ByParam "title" }}
+        <li>
+            <a href="{{ .RelPermalink }}">{{ .Title }}</a>
+        </li>
+        {{ end}}
+    </ul>
+    {{ end }}
+</article>
+{{ end }}
+
+{{ define "toc" }}
+{{ partial "docs/toc" . }}
+{{ end }}

+ 10 - 0
docs/layouts/_default/single.html

@@ -0,0 +1,10 @@
+{{ define "main" }}
+<article class="markdown">
+    {{ partial "breadcrumbs" . }}
+    {{- .Content -}}
+</article>
+{{ end }}
+
+{{ define "toc" }}
+{{ partial "docs/toc" . }}
+{{ end }}

+ 33 - 0
docs/layouts/partials/announcement-banner.html

@@ -0,0 +1,33 @@
+<script>
+  function bannerComponent() {
+    const bannerId = 4;
+    const key = `banner-${bannerId}-dismissed`;
+    const bannerDismissed = localStorage.getItem(key) ?? false;
+    return {
+      visible: !bannerDismissed,
+      dismiss() {
+        this.visible = false;
+        localStorage.setItem(key, true);
+      }
+    };
+  }
+</script>
+<div class="relative bg-gradient-to-r from-blue-600 via-indigo-700 to-blue-900" x-data="bannerComponent()" x-show="visible" x-cloak>
+  <div class="max-w-7xl mx-auto py-1 px-3 sm:px-6 lg:px-8">
+    <div class="pr-16 sm:text-center sm:px-16">
+      <p class="text-xs md:text-sm lg:text-base text-white"  @click="dismiss()">
+          {{ .content | safeHTML }}
+      </p>
+    </div>
+    <div class="absolute inset-y-0 right-0 pt-1 pr-1 flex items-start sm:pt-1 sm:pr-2 sm:items-start">
+      <button type="button" class="flex lg:p-1 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-white"
+      @click="dismiss()">
+        <span class="sr-only">Dismiss</span>
+        <!-- Heroicon name: outline/x -->
+        <svg class="h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
+          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
+        </svg>
+      </button>
+    </div>
+  </div>
+</div>

+ 24 - 0
docs/layouts/partials/breadcrumbs-static.html

@@ -0,0 +1,24 @@
+<nav class="relative max-w-7xl mx-auto py-6 px-6" aria-label="Breadcrumb">
+    <ol class="flex items-center">
+        <li>
+            <div class="flex items-center space-x-4 mr-4">
+                <a href="/"
+                   class="text-sm font-medium text-gray-500 hover:text-gray-700">Home</a>
+            </div>
+        </li>
+        {{ range . }}
+        <li>
+            <div class="flex items-center space-x-4 mr-4">
+                <svg class="flex-shrink-0 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
+                     fill="currentColor" aria-hidden="true">
+                    <path fill-rule="evenodd"
+                          d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
+                          clip-rule="evenodd"/>
+                </svg>
+                <a href="{{ .link }}" class="text-sm font-medium text-gray-500 hover:text-gray-700">{{ .label }}</a>
+            </div>
+        </li>
+        {{ end}}
+
+    </ol>
+</nav>

+ 57 - 0
docs/layouts/partials/breadcrumbs.html

@@ -0,0 +1,57 @@
+<nav class="flex my-6" aria-label="Breadcrumb">
+    <ol class="flex flex-wrap items-center breadcrumbs">
+        {{ template "breadcrumb" dict "currentPage" .Page "id" .UniqueID }}
+    </ol>
+</nav>
+
+<!-- templates -->
+{{ define "breadcrumb" }} {{ if .currentPage.Parent }} {{ template "breadcrumb" dict "currentPage"
+.currentPage.Parent }} {{ else }}
+<li class="list-none">
+    <a href="/" class="block md:mr-3">
+        <svg
+            xmlns="http://www.w3.org/2000/svg"
+            fill="none"
+            viewBox="0 0 24 24"
+            stroke-width="1.5"
+            stroke="currentColor"
+            class="w-4 h-4"
+        >
+            <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
+            />
+        </svg>
+    </a>
+</li>
+{{ end }} {{ $title := cond (eq .currentPage.RelPermalink "/docs/" ) "Docs" (title .currentPage.Title) }} {{
+if (ne .currentPage.Parent .IsHome) }}
+<li class="list-none">
+    <div class="flex items-center md:space-x-3 md:mr-3">
+        {{ if ne $title "Docs" }}
+        <svg
+            class="flex-shrink-0 h-5 w-5 text-gray-400"
+            xmlns="http://www.w3.org/2000/svg"
+            viewBox="0 0 20 20"
+            fill="currentColor"
+            aria-hidden="true"
+        >
+            <path
+                fill-rule="evenodd"
+                d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
+                clip-rule="evenodd"
+            />
+        </svg>
+        {{ end }} {{ if eq .id .currentPage.UniqueID }}
+        <span class="text-sm font-medium text-gray-500 hover:text-gray-700">{{ $title }}</span>
+        {{ else }}
+        <a
+            href="{{ .currentPage.RelPermalink  }}"
+            class="text-sm font-medium text-gray-500 hover:text-gray-700"
+            >{{ $title }}</a
+        >
+        {{ end }}
+    </div>
+</li>
+{{ end }} {{ end }}

+ 3 - 0
docs/layouts/partials/docs/brand.html

@@ -0,0 +1,3 @@
+<h2 class="book-brand">
+  <a href="{{ .Site.BaseURL }}">{{ .Site.Title }}</a>
+</h2>

+ 18 - 0
docs/layouts/partials/docs/git-footer.html

@@ -0,0 +1,18 @@
+{{ if or .GitInfo .Site.Params.BookEditPath }}
+<div class="align-center book-git-footer {{ if not .GitInfo }}justify-end{{ else }}justify-between{{ end }}">
+  {{ with .GitInfo }}
+  <div>
+    <a href="{{ $.Site.Params.BookRepo }}/commit/{{ .Hash }}" title='Last modified {{ .AuthorDate.Local.Format "January 2, 2006 15:04 MST" }} by {{ .AuthorName }}' target="_blank" rel="noopener">
+      <img src="{{ "svg/code-merge.svg" | relURL }}" /> {{ .AuthorDate.Local.Format "Last Modified Jan 2, 2006" }}
+    </a>
+  </div>
+  {{ end }}
+  {{ with .Site.Params.BookEditPath }}
+  <div>
+    <a href="{{ $.Site.Params.BookRepo }}/{{ . }}/{{ $.File.Path }}" target="_blank" rel="noopener">
+      <img src="{{ "svg/code-fork.svg" | relURL }}" /> Edit this page
+    </a>
+  </div>
+  {{ end }}
+</div>
+{{ end }}

+ 9 - 0
docs/layouts/partials/docs/html-head.html

@@ -0,0 +1,9 @@
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>{{- template "title" . }} | Vendure docs</title>
+
+<link rel="preload" as="font" href="/fonts/LexendDeca-Regular.woff2" type="font/woff2" crossorigin="anonymous">
+<link rel="preload" as="font" href="/fonts/LexendDeca-Light.woff2" type="font/woff2" crossorigin="anonymous">
+<link rel="preload" as="font" href="/fonts/Inter-Regular.woff2" type="font/woff2" crossorigin="anonymous">
+<link rel="stylesheet" href="{{ "main.css" | relURL }}">
+<script src="{{ "main.js" | relURL }}"></script>

+ 3 - 0
docs/layouts/partials/docs/menu-bundle.html

@@ -0,0 +1,3 @@
+{{ with .Site.GetPage .Site.Params.BookMenuBundle }}
+  {{- .Content -}}
+{{ end }}

+ 6 - 0
docs/layouts/partials/docs/mobile-header.html

@@ -0,0 +1,6 @@
+<header class="align-center justify-between book-header">
+  <label for="menu-control">
+    <img src="{{ "svg/menu.svg" | relURL }}" />
+  </label>
+  <strong>{{- template "title" . }}</strong>
+</header>

+ 11 - 0
docs/layouts/partials/docs/shared.html

@@ -0,0 +1,11 @@
+<!-- These templates contains some more complex logic and shared between partials-->
+{{ define "title" }}
+  {{- if .Pages -}}
+    {{ $sections := split (trim .Dir "/") "/" }}
+    {{ $title := index ($sections | last 1) 0 | humanize | title }}
+    {{- default $title .Title -}}
+  {{- else -}}
+    {{ $title :=  .File | humanize | title }}
+    {{- default $title .Title -}}
+  {{- end -}}
+{{ end }}

+ 6 - 0
docs/layouts/partials/docs/toc.html

@@ -0,0 +1,6 @@
+{{ $showToC := default (default true .Site.Params.BookShowToC) .Params.showtoc }}
+  {{ if and ($showToC) (.Page.TableOfContents) }}
+  <aside class="book-toc">
+    {{ .Page.TableOfContents }}
+  </aside>
+{{ end }}

+ 213 - 0
docs/layouts/partials/footer.html

@@ -0,0 +1,213 @@
+{{ define "partials/social-link" }}
+  <a href="{{ .url }}" title="{{ .name }}" class="text-gray-400 hover:text-gray-500">
+    <span class="sr-only">{{ .name }}</span>
+    <img class="w-6 h-6" alt="{{ .name }} logo" src="{{ .icon }}"/>
+  </a>
+{{ end }}
+
+
+<footer class="relative bg-gradient-to-br from-gray-700 to-gray-900 relative" aria-labelledby="footerHeading">
+  <h2 id="footerHeading" class="sr-only">Footer</h2>
+  <div class="absolute w-full h-full texture-bg mix-blend-difference"></div>
+  <div class="relative max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:py-16 lg:px-8">
+    <div class="xl:grid xl:grid-cols-3 xl:gap-8">
+      <div class="grid grid-cols-2 gap-8 xl:col-span-2">
+        <div class="md:grid md:grid-cols-2 md:gap-8">
+          <div>
+            <h3 class="text-sm font-semibold text-gray-400 tracking-wider uppercase">
+              Product
+            </h3>
+            <ul class="mt-4 space-y-4">
+              <li>
+                <a href="/features" class="text-base text-gray-300 hover:text-white">
+                  Features
+                </a>
+              </li>
+
+              <li>
+                <a href="/case-study" class="text-base text-gray-300 hover:text-white">
+                  Case studies
+                </a>
+              </li>
+
+              <li>
+                <a href="/plugins" class="text-base text-gray-300 hover:text-white">
+                  Plugins
+                </a>
+              </li>
+
+
+              <li>
+                <a href="/integration" class="text-base text-gray-300 hover:text-white">
+                  Integrations
+                </a>
+              </li>
+
+              <li>
+                <a href="https://demo.vendure.io" class="text-base text-gray-300 hover:text-white">
+                  Demo
+                </a>
+              </li>
+            </ul>
+          </div>
+          <div class="mt-12 md:mt-0">
+            <h3 class="text-sm font-semibold text-gray-400 tracking-wider uppercase">
+              Services
+            </h3>
+            <ul class="mt-4 space-y-4">
+              <li>
+                <a href="/support" class="text-base text-gray-300 hover:text-white">
+                  Support
+                </a>
+              </li>
+              <li>
+                <a href="/partners" class="text-base text-gray-300 hover:text-white">
+                  Become a Partner
+                </a>
+              </li>
+              <li>
+                <a href="/cert-check" class="text-base text-gray-300 hover:text-white">
+                  Partner Certification Check
+                </a>
+              </li>
+            </ul>
+          </div>
+        </div>
+        <div class="md:grid md:grid-cols-2 md:gap-8">
+          <div>
+            <h3 class="text-sm font-semibold text-gray-400 tracking-wider uppercase">
+              Documentation
+            </h3>
+            <ul class="mt-4 space-y-4">
+              <li>
+                <a href="/docs/getting-started/" class="text-base text-gray-300 hover:text-white">
+                  Getting Started
+                </a>
+              </li>
+
+              <li>
+                <a href="/docs/developer-guide/" class="text-base text-gray-300 hover:text-white">
+                  Developer Guide
+                </a>
+              </li>
+
+              <li>
+                <a href="/docs/graphql-api/" class="text-base text-gray-300 hover:text-white">
+                  GraphQL API
+                </a>
+              </li>
+            </ul>
+          </div>
+          <div class="mt-12 md:mt-0">
+            <h3 class="text-sm font-semibold text-gray-400 tracking-wider uppercase">
+              Community
+            </h3>
+            <ul class="mt-4 space-y-4">
+              <li>
+                <a href="https://github.com/vendure-ecommerce/vendure" class="text-base text-gray-300 hover:text-white">
+                  GitHub
+                </a>
+              </li>
+
+              <li>
+                <a href="https://github.com/vendure-ecommerce/vendure/discussions" class="text-base text-gray-300 hover:text-white">
+                  Support Forum
+                </a>
+              </li>
+
+              <li>
+                <a href="https://www.vendure.io/community" class="text-base text-gray-300 hover:text-white">
+                  Discord Community
+                </a>
+              </li>
+
+              <li>
+                <a href="/contributors" class="text-base text-gray-300 hover:text-white">
+                  Sponsors & Contributors
+                </a>
+              </li>
+            </ul>
+          </div>
+        </div>
+      </div>
+      <div class="mt-8 xl:mt-0">
+        <h3 class="text-sm font-semibold text-gray-400 tracking-wider uppercase">
+          Subscribe to our newsletter
+        </h3>
+        <p class="mt-4 text-base text-gray-300">
+          Get important Vendure news and announcements direct to your inbox.
+        </p>
+        <!-- Begin Mailchimp Signup Form -->
+        <div id="mc_embed_signup">
+          <form class="mt-4 validate"
+                action="https://vendure.us1.list-manage.com/subscribe/post?u=967048b8dcdf53aea61cf1a28&amp;id=6802bb1d9d"
+                method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form"
+                target="_blank"
+                novalidate>
+            <div id="mc_embed_signup_scroll" class="sm:flex sm:max-w-md space-x-1">
+
+              <input type="email" value="" name="EMAIL" id="mce-EMAIL" placeholder="email address" required
+                     class="appearance-none min-w-0 w-full bg-white border border-transparent rounded-md py-2 px-4 text-base text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white focus:border-white focus:placeholder-gray-400">
+              <!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups-->
+              <div style="position: absolute; left: -5000px;" aria-hidden="true"><input type="text"
+                                                                                        name="b_967048b8dcdf53aea61cf1a28_6802bb1d9d"
+                                                                                        tabindex="-1" value=""></div>
+              <input type="submit" value="Subscribe" name="subscribe" id="mc-embedded-subscribe"
+                     class="w-32 btn-primary">
+            </div>
+          </form>
+        </div>
+        <!--End mc_embed_signup-->
+      </div>
+    </div>
+    <div class="mt-8 border-t border-gray-700 pt-8 md:flex md:items-center md:justify-between">
+      <div class="md:order-2">
+        <div class="flex space-x-6">
+            {{ partial "social-link" (dict
+            "url" "https://github.com/vendure-ecommerce/vendure"
+            "name" "GitHub"
+            "icon" "/svg/icon-github-inverse.svg"
+            ) }}
+            {{ partial "social-link" (dict
+            "url" "https://www.npmjs.com/~vendure"
+            "name" "npm"
+            "icon" "/svg/icon-npm.svg"
+            ) }}
+            {{ partial "social-link" (dict
+            "url" "https://twitter.com/vendure_io"
+            "name" "Twitter"
+            "icon" "/svg/icon-twitter.svg"
+            ) }}
+            {{ partial "social-link" (dict
+            "url" "https://www.vendure.io/discord"
+            "name" "Discord"
+            "icon" "/logo/discord-logo.png"
+            ) }}
+            {{ partial "social-link" (dict
+            "url" "https://www.youtube.com/channel/UCZuBR2NrUKOq8M9_mDYP8PA"
+            "name" "YouTube"
+            "icon" "/svg/icon-youtube.svg"
+            ) }}
+            {{ partial "social-link" (dict
+            "url" "mailto:contact@vendure.io"
+            "name" "Email"
+            "icon" "/svg/clr-icon-email-light.svg"
+            ) }}
+        </div>
+        <div class="flex mt-4 space-x-4 opacity-90">
+          <a href="https://github.com/vendure-ecommerce/vendure"><img class="" alt="GitHub star counter"
+                                                                      src="https://img.shields.io/github/stars/vendure-ecommerce/vendure.svg?style=social"></a>
+          <a href="https://www.npmjs.com/package/@vendure/core"><img class="" alt="npm version"
+                                                                     src="https://img.shields.io/npm/v/@vendure/core"></a>
+        </div>
+      </div>
+      <div class="mt-8 text-base text-gray-400 md:mt-0 md:order-1">
+        <p>&copy; {{ now.Format "2006" }} Vendure GmbH. All rights reserved.
+        </p>
+        <p class="text-gray-500">Wiedner Gürtel 12/1/2, 1040 Vienna, Austria</p>
+        <a class="text-gray-400 hover:underline mr-8" href="/impressum">Impressum</a>
+        <a class="text-gray-400 hover:underline" href="/branding">Logo resources & trademark</a>
+        </p>
+      </div>
+    </div>
+</footer>

+ 14 - 0
docs/layouts/partials/light-triangle-pattern.html

@@ -0,0 +1,14 @@
+<svg class="absolute hidden lg:block top-0 left-full transform -translate-x-1/2 -translate-y-3/4 lg:left-auto lg:right-full lg:translate-x-full lg:translate-y-1/4"
+     width='300' height='300' xmlns='http://www.w3.org/2000/svg'>
+  <defs>
+    <pattern id='triangles-light' patternUnits='userSpaceOnUse' width='40' height='40'
+             patternTransform='scale(1) rotate(0)'>
+      <rect x='0' y='0' width='100%' height='100%' fill='hsla(0, 0%, 86%, 0)'/>
+      <path d='M0 0l10 20L20 0H0zm10 20l10 20 10-20H10z' stroke-width='1' stroke='none'
+            fill='hsla(189, 0%, 74%, 0.1)'/>
+      <path d='M20 0l10 20L40 0zm10 20l10 20 10-20zm-40 0L0 40l10-20z' stroke-width='1'
+            stroke='none' fill='hsla(340, 0%, 72%, 0.1)'/>
+    </pattern>
+  </defs>
+  <rect width='100%' height='100%' transform='translate(0,0)' fill='url(#triangles-light)'/>
+</svg>

+ 36 - 0
docs/layouts/partials/menu-filetree.html

@@ -0,0 +1,36 @@
+<ul>
+    <li>{{ template "book-section" (dict "Section" .Site.Sections "CurrentPage" $.Permalink) }}</li>
+</ul>
+
+{{ define "book-section" }} <!-- Single section of menu (recursive) -->
+<ul {{ if .Expanded }}class="expanded"{{ end }}>
+    {{ range (.Section.ByParam "Title").ByParam "Weight" }}
+        {{ if ne .Title "Search Index" }}
+            {{ if and (eq .Kind "section") (or (gt (len .Pages) 0) (gt (len .Sections) 0) ) }}
+                <li class="section">
+                    {{ $expanded := in $.CurrentPage .Permalink }}
+                    <div class="section-link">
+                        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 section-icon {{ if $expanded}}expanded{{ end }}">
+                          <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
+                        </svg>
+                        {{ template "book-page-link" (dict "Page" . "CurrentPage" $.CurrentPage) }}
+                    </div>
+                    {{ template "book-section" (dict "Section" .Pages "CurrentPage" $.CurrentPage "Expanded" $expanded) }}
+                </li>
+            {{ else }}
+                <li>
+                    {{ template "book-page-link" (dict "Page" . "CurrentPage" $.CurrentPage) }}
+                </li>
+            {{ end }}
+        {{end }}
+    {{ end }}
+  </ul>
+{{ end }}
+
+{{ define "book-page-link" }}
+{{- with .Page -}}
+<a href="{{ .RelPermalink }}" {{- if eq $.CurrentPage .Permalink }} class="text-blue-700 active"{{ end }}>
+  {{- template "title" . -}}
+</a>
+{{- end -}}
+{{ end }}

+ 27 - 0
docs/layouts/partials/top-bar.html

@@ -0,0 +1,27 @@
+<div class="z-30 transition-colors top-0"
+     :class="{ 'bg-transparent bg-none absolute w-full': isLandingPage, 'bg-white border-b border-dotted border-blue-200': !isLandingPage }"
+     x-data="{ ...Components.popover(false), isLandingPage: {{ default false .isLandingPage }} }">
+
+    <div :class="{ 'hidden': isLandingPage }"
+         class="absolute w-full h-1 bg-gradient-to-r from-blue-300 via-green-300 to-blue-100"></div>
+    {{ partial "announcement-banner" (dict
+        "content" `
+        🚨 Announcing <a href="/blog/2023/04/announcing-vendure-2.0-beta/" class="text-white font-medium underline">Vendure v2 Beta<span aria-hidden="true">&rarr;</span></a>
+        `) }}
+    <div class="max-w-screen-xl mx-auto px-4 py-4 sm:px-6 md:pr-10">
+        <div class="flex justify-between items-center py-2 md:space-x-10">
+
+            <div class="flex justify-start lg:w-0 lg:flex-1">
+                {{ if not .isLandingPage }}
+                <a class="logo flex space-x-4" href="/" title="back to landing page">
+                    <img class="w-10" src="/logo.png" alt="Vendure logo"/>
+                    <div class="font-wordmark font-bold text-blue-500 text-3xl mt-0">vendure</div>
+                </a>
+                {{ end }}
+            </div>
+            <div class="flex text-gray-500">
+                Documentation
+            </div>
+        </div>
+    </div>
+</div>

+ 13 - 0
docs/layouts/searchindex/searchindex.html

@@ -0,0 +1,13 @@
+{
+result :[
+    {{- range .Site.Pages -}}
+        {{ $titles := findRE "<h(1|2).*?>(.|\n)*?</h(1|2)>" .Content -}}
+        {{ $plain := apply $titles "plainify" "." -}}
+        {{ $cleaned := $plain | complement (slice "constructor" "Signature" "Members") -}}
+        {{ $quoted := apply $cleaned "replaceRE" ".*" "'$0'" "." -}}
+        {{ $section := cond (in .Dir "typescript-api") "config" (cond (in .Dir "graphql-api") "gql" (cond (in .Dir "user-guide") "user-guide" "developer-guide")) }}
+
+        { section: '{{ $section }}', parent: '{{ if .Parent }}{{ title .Parent.Title }}{{ end }}', title: '{{ .Title }}', headings: [{{ delimit $quoted ", " }}], url: '{{ .RelPermalink }}' },
+    {{- end }}
+]
+}

+ 11 - 0
docs/layouts/shortcodes/alert.html

@@ -0,0 +1,11 @@
+{{ $type := index .Params 0 | default "primary" }}
+<div x-data="{ type: '{{ $type }}' }" class='border rounded px-4 py-2 my-6'
+    :class="{
+        'bg-blue-50 text-blue-900 border-blue-200': type === 'primary',
+        'bg-red-50 text-red-900 border-red-200': type === 'danger',
+        'bg-yellow-50 text-yellow-900 border-yellow-200': type === 'warning',
+        'bg-green-50 text-green-900 border-green-200': type === 'success',
+    }"
+>
+    <p>{{ .Inner | markdownify }}</p>
+</div>

+ 15 - 0
docs/layouts/shortcodes/generation-info.html

@@ -0,0 +1,15 @@
+<div class="generation-info flex flex-wrap">
+    <span class="label">Package:</span> <a href="https://www.npmjs.com/package/{{ .Get "packageName" }}">{{ .Get "packageName" }}</a>
+    <span class="label file">File:</span> <a href="https://github.com/vendure-ecommerce/vendure/blob/master/{{ .Get "sourceFile" }}#L{{ .Get "sourceLine" }}">{{  index (last 1 (split (.Get "sourceFile") "/")) 0 }}</a>
+    {{- if isset .Params "since" -}}
+    <div class="flex ml-2 bg-yellow-100 px-1 rounded-sm" title="This API was added in v{{ .Get "since" | safeHTML }}">
+        <div class="text-yellow-700">v{{ .Get "since" | safeHTML }}</div>
+    </div>
+    {{ end }}
+    {{- if isset .Params "experimental" -}}
+    <div class="since ml-2 bg-yellow-100 px-2 rounded-sm" title="This API is experimental and may change in a future release">
+       <div class="text-yellow-700">experimental</div>
+    </div>
+    {{ end }}
+</div>
+

+ 3 - 0
docs/layouts/shortcodes/gql-enum-values.html

@@ -0,0 +1,3 @@
+<div class="gql-enum-values">
+    {{ .Inner }}
+</div>

+ 3 - 0
docs/layouts/shortcodes/gql-fields.html

@@ -0,0 +1,3 @@
+<div class="gql-fields">
+    {{ .Inner }}
+</div>

+ 3 - 0
docs/layouts/shortcodes/image.html

@@ -0,0 +1,3 @@
+<figure class='{{ if .Get "flat" }}flat{{ end }}'>
+    <img src='{{ .Get "src" }}'>
+</figure>

+ 3 - 0
docs/layouts/shortcodes/member-description.html

@@ -0,0 +1,3 @@
+<div class="member-description">
+    {{ .Inner | markdownify }}
+</div>

+ 26 - 0
docs/layouts/shortcodes/member-info.html

@@ -0,0 +1,26 @@
+<div class="member-info">
+    <div class="kind">
+        <div class="kind-label">{{ .Get "kind" | safeHTML }}</div>
+    </div>
+
+    {{- if isset .Params "since" -}}
+    <div class="since mr-2 bg-yellow-100 px-2 rounded-sm" title="This API was added in v{{ .Get "since" | safeHTML }}">
+        <div class="text-yellow-700">v{{ .Get "since" | safeHTML }}</div>
+    </div>
+    {{ end }}
+    {{- if isset .Params "experimental" -}}
+    <div class="since mr-2 bg-yellow-100 px-2 rounded-sm" title="This API is experimental and may change in a future release">
+        <div class="text-yellow-700">experimental</div>
+    </div>
+    {{ end }}
+    <div class="type">
+        <div class="label">type:</div>
+        <code>{{ .Get "type" | safeHTML }}</code>
+    </div>
+    {{- if isset .Params "default" -}}
+    <div class="default">
+        <div class="label">default:</div>
+        <code>{{ .Get "default" | safeHTML }}</code>
+    </div>
+    {{ end }}
+</div>

+ 21 - 0
docs/layouts/shortcodes/pull-quote.html

@@ -0,0 +1,21 @@
+<div class="relative mx-2 sm:mx-12 mt-16 mb-10">
+    <svg class="absolute top-0 left-0 transform -translate-y-6 -translate-x-12 sm:-translate-y-6 h-16 w-16 sm:h-16 sm:w-16 text-blue-800 opacity-10"
+         stroke="currentColor" fill="none" viewBox="0 0 144 144" aria-hidden="true">
+        <path stroke-width="2"
+              d="M41.485 15C17.753 31.753 1 59.208 1 89.455c0 24.664 14.891 39.09 32.109 39.09 16.287 0 28.386-13.03 28.386-28.387 0-15.356-10.703-26.524-24.663-26.524-2.792 0-6.515.465-7.446.93 2.327-15.821 17.218-34.435 32.11-43.742L41.485 15zm80.04 0c-23.268 16.753-40.02 44.208-40.02 74.455 0 24.664 14.891 39.09 32.109 39.09 15.822 0 28.386-13.03 28.386-28.387 0-15.356-11.168-26.524-25.129-26.524-2.792 0-6.049.465-6.98.93 2.327-15.821 16.753-34.435 31.644-43.742L121.525 15z"/>
+    </svg>
+    <div class="text-lg      sm:leading-9  text-blue-800">
+        {{ .Get "text" }}
+    </div>
+    <div class="flex mt-2">
+        {{ if .Get "avatar" }}
+        <div class="mr-4">
+            <img class="w-12 h-12 rounded-full" src='{{ .Get "avatar" }}'>
+        </div>
+        {{ end }}
+        <div>
+            {{ if .Get "name" }}<div class="text-gray-600">{{ .Get "name" }}</div>{{ end }}
+            {{ if .Get "position" }}<div class="mt-1 text-gray-500">{{ .Get "position" }}</div>{{ end }}
+        </div>
+    </div>
+</div>

+ 4 - 0
docs/layouts/shortcodes/shop-api-operation.html

@@ -0,0 +1,4 @@
+{{- $operationName := .Get "operation" -}}
+{{- $operationSegment := cond (eq (.Get "type") "query") "queries" "mutations" -}}
+{{- $link := (print "/docs/graphql-api/shop/" $operationSegment  "#" (lower $operationName) ) -}}
+<em>{{ .Get "type" }}</em> <a href="{{ $link }}"><code>{{ $operationName }}</code></a>

+ 3 - 0
docs/layouts/shortcodes/tab.html

@@ -0,0 +1,3 @@
+<div class="tab" title="{{ .Get 0 }}">
+  {{ .Inner }}
+</div>

+ 10 - 0
docs/netlify.toml

@@ -0,0 +1,10 @@
+[[redirects]]
+  from = "/slack"
+  to = "https://join.slack.com/t/vendure-ecommerce/shared_invite/zt-1sk0pstzq-14fPu8d3CkySui5i~21_2g"
+  status = 200
+  force = true
+[[redirects]]
+  from = "/community"
+  to = "https://discord.gg/4u2aSCzDDX"
+  status = 200
+  force = true

+ 37 - 0
docs/package.json

@@ -0,0 +1,37 @@
+{
+  "name": "vendure-docs",
+  "version": "1.0.0",
+  "description": "Documentation site for Vendure",
+  "main": "index.js",
+  "author": "Michael Bromley <michael@michaelbromley.co.uk>",
+  "license": "MIT",
+  "private": true,
+  "scripts": {
+    "watch": "yarn webpack -w",
+    "build": "yarn webpack && hugo"
+  },
+  "dependencies": {
+    "@tailwindcss/aspect-ratio": "^0.2.0",
+    "@tailwindcss/forms": "^0.3.2",
+    "@tailwindcss/typography": "^0.4.0",
+    "@types/node": "^14.14.37",
+    "@types/webpack": "^4.41.11",
+    "alpinejs": "^2.8.2",
+    "autoprefixer": "^10.2.5",
+    "css-loader": "^3.5.2",
+    "fuzzy": "^0.1.3",
+    "mini-css-extract-plugin": "^0.9.0",
+    "node-fetch": "^2.6.1",
+    "node-sass": "^6.0.1",
+    "postcss": "^8.2.10",
+    "postcss-loader": "^4.2.0",
+    "sass-loader": "^10.2.1",
+    "tailwindcss": "^2.1.1",
+    "terser-webpack-plugin": "^4.2.3",
+    "ts-loader": "^6.2.2",
+    "ts-node": "^9.1.1",
+    "typescript": "^4.2.4",
+    "webpack": "^4.42.1",
+    "webpack-cli": "^3.3.11"
+  }
+}

+ 7 - 0
docs/postcss.config.js

@@ -0,0 +1,7 @@
+// postcss.config.js
+module.exports = {
+  plugins: [
+    require('tailwindcss'),
+    require('autoprefixer'),
+  ]
+}

BIN
docs/static/branding/cube-logo-300.png


BIN
docs/static/branding/cube-logo-800.png


+ 25 - 0
docs/static/branding/cube-logo-vector.svg

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 1600 1600" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <g transform="matrix(2.30942,0,0,2.34847,10.1798,-40.8178)">
+        <path d="M11,169.979L133.025,259.614L132.823,427.72L11,316.659L11,169.979Z" style="fill:rgb(48,198,253);"/>
+    </g>
+    <g transform="matrix(2.30942,0,0,2.34847,10.1798,-40.8178)">
+        <path d="M11,169.979L179.007,98L306.922,169.273L133.025,259.614L11,169.979Z" style="fill:rgb(23,193,255);"/>
+    </g>
+    <g transform="matrix(2.30942,0,0,2.34847,10.1798,-40.8178)">
+        <path d="M168.441,284.368L343.012,415.263L342.923,618.057L168.441,459.974L168.441,284.368Z" style="fill:rgb(48,198,253);"/>
+    </g>
+    <g transform="matrix(2.30942,0,0,2.34847,10.1798,-40.8178)">
+        <path d="M342,188.285L517.583,284.368L343.012,415.299L168.441,284.368L342,188.285Z" style="fill:rgb(23,193,255);"/>
+    </g>
+    <g transform="matrix(2.30942,0,0,2.34847,10.1798,-40.8178)">
+        <path d="M343.012,618.057L517.148,459.8L517.583,284.368L342.923,415.263L343.012,618.057Z" style="fill:rgb(19,183,243);"/>
+    </g>
+    <g transform="matrix(2.30942,0,0,2.34847,10.1798,-40.8178)">
+        <path d="M377.092,169.979L504.527,98L673,168.739L551.102,259.614L377.092,169.979Z" style="fill:rgb(23,193,255);"/>
+    </g>
+    <g transform="matrix(2.30942,0,0,2.34847,10.1798,-40.8178)">
+        <path d="M551.102,259.614L550.887,429.427L673,316.87L673,168.739L551.102,259.614Z" style="fill:rgb(19,183,243);"/>
+    </g>
+</svg>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
docs/static/branding/wordmark-logo-vector.svg


BIN
docs/static/branding/wordmark-logo.png


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
docs/static/branding/wordmark-vector.svg


BIN
docs/static/branding/wordmark.png


BIN
docs/static/favicon.ico


BIN
docs/static/fonts/Inter-Bold.woff2


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio