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 whencontext.nojsis true. -
No
onclick,onload,onerroror 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.
- your theme's
nojs/<name>.html - the default theme's
nojs/<name>.html -
your theme's own template for that name, through the
htmlortemplatesmap in yourtheme.json - 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.htmlfile 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.