Degoog Docs

Degoog No-JS support

Opting a slot, a command or a theme into the script-free page at /nojs.

/nojs is a second, server-rendered search page that ships no JavaScript. What it is and how an admin turns it on is covered in The no-JS page. This page is the contract your extension codes against.

Default deny, and why

The page makes one promise to its visitors: the HTML it sends contains no <script> tag and no inline on*= handler. Slot panels and command output are third-party HTML that Degoog concatenates into that page. If the promise is to mean anything, Degoog cannot take your word for it by default.

So the default is deny. A slot or a command appears on /nojs only when it sets supportsNojs: true, and the check is a strict === true. A truthy string will not do it.

if (nojs && plugin.supportsNojs !== true) continue;

Theme templates are sanitised on the way in, because Degoog knows what they are. Slot and command HTML is not: it arrives already assembled, and stripping tags out of it would change what your extension said rather than tell you it was wrong. The flag is you telling Degoog that every branch of your execute returns markup free of both, which is a claim only you can check.

Slots

Two fields. SlotPlugin.supportsNojs opts the panel in, and SlotPluginContext.nojs tells your execute which page it is rendering for. The context field is only present on the no-JS page, so test it as context?.nojs and treat undefined as the normal page.

const slot: SlotPlugin = {
  id: "my-panel",
  name: "My panel",
  description: "…",
  position: SlotPanelPosition.KnowledgePanel,
  supportsNojs: true,
  trigger: (query) => query.length > 2,
  async execute(query, context) {
    if (context?.nojs) return { title: "My panel", html: staticMarkup(query) };
    return { title: "My panel", html: interactiveMarkup(query) };
  },
};

Everything else behaves as it does on the normal page. Your trigger still runs, waitForResults still decides whether you get context.results, the disabled check and the search type filter still apply, and the position you chose is respected.

Respected means the same container the normal page uses. FullWidthAboveResults, AboveResults, BelowResults, AboveSidebar and BelowSidebar are rendered into #slot-full-width-above-results, #slot-above-results, #slot-below-results, #slot-above-sidebar and #slot-below-sidebar, wrapped in the same .results-slot-panel block with the same data-slot and data-grid attributes. AtAGlance goes into #at-a-glance as raw HTML with no wrapper, because the background request that normally fetches it needs a browser to make it. KnowledgePanel panels are prepended into #results-sidebar above the engine breakdown, each in a sidebar accordion, which is where the Wikipedia panel lands.

The one structural difference is the accordion itself. The normal page builds it from a <button> and a listener, so the no-JS page uses <details> and <summary> carrying the same classes, open by default.

Panels keep their data-slot attribute, so the CSS you already wrote against [data-slot="my-panel"] applies here too. Your style.css is loaded on the no-JS page like any other page.

A slot that has not opted in is skipped in silence. Nobody asked for the panel, so nobody is owed an explanation for its absence.

Bang commands

Same two fields: BangCommand.supportsNojs and CommandContext.nojs. The context you receive is clientIp, page, signProxyUrl and nojs: true.

const command: BangCommand = {
  name: "My command",
  description: "…",
  trigger: "mine",
  supportsNojs: true,
  async execute(args, context) {
    const copyButton = context?.nojs ? "" : `<button class="copy">Copy</button>`;
    return { title: "My command", html: `<pre>${answer(args)}</pre>${copyButton}` };
  },
};

A command that has not opted in renders a notice telling the user to ask the extension's developer to add support. That is deliberately louder than the slot case: the user typed !mine, they are waiting for an answer, and silence would look like the instance is broken. Disabled commands and commands that throw get their own notices through the same mechanism.

totalPages is honoured, but there is no pagination widget around command output on this page yet.

Before you set the flag

  • No <script> tag, inline or external, in any branch that can run when context.nojs is true.
  • No onclick, onload, onerror or any other inline handler, including on images.
  • Nothing that only works once client code hydrates it: tab strips, copy buttons, collapsible sections driven by a listener, anything that starts empty and fills in later.
  • External images still go through context.signProxyUrl. The image proxy is the same on both pages.
  • CSS is fine. So are links, forms, <details> and :hover. Plenty of interactivity survives without scripts.
  • If a branch genuinely cannot work without JavaScript, do not set the flag. See the speedtest example.

Five that opted in, and one that did not

All six ship with Degoog, and between them they cover every shape this decision takes.

at-a-glance and wikipedia: nothing to change

Both slots already produced plain markup. The Wikipedia panel is a summary, a thumbnail through the signed image proxy and a link. At a glance is an extracted snippet from the top results. Neither had a script in it, so opting in was one line each and no branch on context.nojs at all. If your panel is in this shape, so is your change.

help: same data, simpler shell

!help normally renders a search box, a row of category tabs and one visible panel, with client code filtering rows and switching tabs. None of that works here, so under context.nojs it drops the search box and the tab strip and marks every category panel active. You get the whole command list at once, longer to scroll and complete. The rows themselves are the same markup either way.

ip: the server already knew the answer

!ip with no argument has to work out who is asking. On the normal page it ships an inline script that calls ipify from the browser and posts the result back, which is the right call there because it sees the visitor's address rather than the server's. On /nojs that script cannot run, so the command falls back to context.clientIp, which the server has had all along. When that address is missing or local it returns the same hint text the script would have shown on failure, instead of a spinner that never resolves.

This is the most interesting of the five. The no-JS branch is not a degraded version of the feature, it is a different route to the same answer.

uuid: drop what would be inert

!uuid generates identifiers on the server and prints them, so the output itself needs nothing. The only casualty is the per-row Copy button, which under context.nojs is omitted rather than rendered dead. A button that does nothing when clicked is worse than no button.

speedtest: declining is a correct answer

!speedtest does not set supportsNojs, and it should not. Measuring throughput means downloading a payload and timing it, in the browser, which is running code in the browser. There is no static markup that measures anything. A server-side number would describe the server's connection, not yours, and quietly answering a different question than the one asked is worse than not answering.

So it opts out, and a user who types !speedtest on /nojs gets the notice. That notice is the feature working. If your command is in this category, leave the flag off and let the page tell the truth about it.

Theme overrides

The no-JS page is built out of the same shells and partials your theme already ships. It loads index.html and search.html, fills the empty #id containers in them server side the way the client fills them at runtime, and renders the partials named in your theme.json. Restyle the normal page and the no-JS page follows. You do not have to write it twice.

Where a section genuinely cannot survive without scripts, drop a nojs/<name>.html file into your theme folder. There is nothing to add to theme.json. Each name resolves down four tiers and stops at the first hit.

  1. your theme's nojs/<name>.html
  2. the default theme's nojs/<name>.html
  3. your theme's own template for that name, through the html or templates map in your theme.json
  4. the default theme's template for that name

Tier three is the one that matters. Ship a result.html and no nojs/ folder and /nojs uses your result card, not degoog's.

my-theme/
  theme.json
  style.css
  search-templates/
    result.html       <- used on both pages
  nojs/
    tabs.html         <- only if you need a different filter form
Name Where it comes from What it is
layout html.layout The HTML shell. The same one the normal page uses, with its scripts stripped and the no-JS stylesheets added.
index html.index The home shell. Its empty containers are filled server side.
search html.search The results shell, containers and all.
home-header templates.home-header The settings gear, dropped into #header.
home-search templates.home-search The home search form. It is already a real <form>; the server only rewrites its action and method and removes the feeling-lucky button, which cannot work here.
home-footer templates.home-footer Version, repo and docs links. The server appends one more link back to the full app.
search-header templates.search-header Small logo plus the results search bar. The bar is not a form on the normal page, so the server wraps it in one, names the input q and makes the submit button a real submit button.
result templates.result One web result, favicon, engine tags, insecure badge and video overlay included. The actions menu collapses because the server passes show_actions: false.
image-card templates.image-card One tile in the image grid, wrapped in a link since the lightbox needs scripts. Video searches use result, exactly as the normal page does.
logo nojs/logo.html The home logo. The default theme renders its letters in binary in a monospace face here, which is a deliberate difference rather than a thing to inherit.
tabs nojs/tabs.html The tab row and the filter form. The normal tools panel is <button>s driving dropdowns, so this one is a <details> whose <summary> is #results-tabs, with a real <form> of <select>s below it. Same class names as the normal one, so the same CSS applies.
pagination nojs/pagination.html Page links. The client builds pagination in code, so there is no normal template to inherit. Only rendered when there is more than one page.

Two kinds of placeholder appear in these files. __UPPER_SNAKE__ tokens are substituted by the server: __PAGE_CONTENT__ and __BODY_CLASS__ in the layout, and __THEME_CSS__, __CUSTOM_CSS__, __LANG_ATTR__, __THEME_ATTRS__, __RTL_SUPPORT__ and __APP_VERSION__ in the head. __PLUGIN_ASSETS__ and __THEME_TEMPLATES__ resolve to nothing, because neither has anything to do on a page with no client code. Alongside them, result, tab, filter and pagination templates use the same small mustache-style engine as the rest of the theme system, with {{ value }}, {{#if key}}, {{^if key}} and {{#each list}}. Values are HTML escaped for you. Translated strings use {{t:namespace.key}}, and __THEME_PATH__ resolves to your theme's asset directory as it does everywhere else.

When an override does not load

Overrides are matched by exact filename. Name a file something degoog never asks for and the page still works, quietly, using the tier below. It warns twice over rather than leaving no trace.

  • The first time a theme's nojs/ folder is read, any .html file in it that is not one of the names above produces a warning naming the stray files and the names degoog accepts. This is the typo case, and it is the one that used to cost people an afternoon.
  • A file that is there but cannot be read or parsed warns on every load and falls back to the tier below. Only the file genuinely being absent stays quiet, because not overriding a template is the normal case.

You do not have to strip your own scripts. Every template the no-JS page renders, the layout included, is run through a sanitiser that removes <script> blocks, inline on*= handlers and module preloads before anything else touches it. That is why your normal layout.html can be reused unchanged. It also means a section that only works once a script hydrates it will render empty rather than render broken, so check what your template looks like with its behaviour taken away. The one thing the sanitiser cannot clean is slot and command HTML, which is why those are opt-in.

The CSS bot check needs nothing from your layout either. When the admin turns it on, the server injects the signed stylesheet link before </head> itself, along with the no-JS and icon stylesheets.

Dark mode needs nothing from you. The page ships no theme switcher and no cookie, so prefers-color-scheme in the core stylesheet decides, unless the admin set an instance default theme. If your style.css hardcodes light colours, the no-JS page is where that shows up first.