diff --git a/.github/workflows/docker-publish.yaml b/.github/workflows/docker-publish.yaml
new file mode 100644
index 000000000..bed46cc7f
--- /dev/null
+++ b/.github/workflows/docker-publish.yaml
@@ -0,0 +1,41 @@
+name: Create and publish a Docker image
+
+on:
+ push:
+ branches: ['hugo']
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
+
+jobs:
+ build-and-push-image:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..ac5e8de26
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,10 @@
+FROM alpine:3.16
+
+RUN apk add --no-cache go hugo git make perl
+RUN go install github.com/jackyzha0/hugo-obsidian@latest
+ENV PATH="/root/go/bin:$PATH"
+RUN git clone https://github.com/jackyzha0/quartz.git /quartz
+
+WORKDIR /quartz
+
+CMD ["make", "serve"]
diff --git a/Makefile b/Makefile
index 5a88927fa..50d542ac0 100644
--- a/Makefile
+++ b/Makefile
@@ -17,4 +17,8 @@ update-force: ## Forcefully pull all changes and don't ask to patch
git checkout upstream/hugo -- layouts .github Makefile assets/js assets/styles/base.scss assets/styles/darkmode.scss config.toml data
serve: ## Serve Quartz locally
- hugo-obsidian -input=content -output=assets/indices -index -root=. && hugo server --enableGitInfo --minify
+ hugo-obsidian -input=content -output=assets/indices -index -root=.
+ hugo server --enableGitInfo --minify --bind=$(or $(HUGO_BIND),0.0.0.0) --baseURL=$(or $(HUGO_BASEURL),http://localhost) --port=$(or $(HUGO_PORT),1313) --appendPort=$(or $(HUGO_APPENDPORT),true)
+
+docker: ## Serve locally using Docker
+ docker run -it --volume=$(shell pwd):/quartz -p 1313:1313 ghcr.io/jackyzha0/quartz:hugo
diff --git a/assets/js/semantic-search.js b/assets/js/semantic-search.js
index 4382817ce..bfe102e0b 100644
--- a/assets/js/semantic-search.js
+++ b/assets/js/semantic-search.js
@@ -1,38 +1,54 @@
-const apiKey = "{{$.Site.Data.config.operandApiKey}}"
+// Note: Currently, we use the REST API for Operand because of some unpkg/webpack issues.
+// In the future, we'd like to use the SDK (https://github.com/operandinc/typescript-sdk).
+// If someone knows how to do this w/o breaking the Operand typescript-sdk for npm users,
+// please let Morgan (@morgallant) and/or (@_jzhao) know! <3
+
+const apiKey = "{{$.Site.Data.config.search.operandApiKey}}"
+const indexId = "{{$.Site.Data.config.search.operandIndexId}}"
+
+function parseSearchResults(searchResults) {
+ return searchResults.matches.map((m) => ({
+ content: m.content,
+ title: searchResults.objects[m.objectId].properties.properties._title.text,
+ url: searchResults.objects[m.objectId].properties.properties._url.text,
+ }))
+}
async function searchContents(query) {
- const response = await fetch('https://prod.operand.ai/v3/search/objects', {
- method: 'POST',
+ const result = await fetch("https://api.operand.ai/operand.v1.ObjectService/SearchWithin", {
+ method: "POST",
headers: {
- 'Content-Type': 'application/json',
- Authorization: apiKey,
+ "Content-Type": "application/json",
+ Authorization: `${apiKey}`,
+ "Operand-Index-ID": `${indexId}`,
},
body: JSON.stringify({
- query,
- max: 10
+ query: query,
+ limit: 8,
}),
- });
- return (await response.json());
+ })
+ if (result.ok) {
+ return parseSearchResults(await result.json())
+ } else {
+ console.error(result)
+ }
}
function debounce(func, timeout = 200) {
- let timer;
+ let timer
return (...args) => {
clearTimeout(timer)
- timer = setTimeout(() => { func.apply(this, args); }, timeout)
- };
+ timer = setTimeout(() => {
+ func.apply(this, args)
+ }, timeout)
+ }
}
-registerHandlers(debounce((e) => {
- term = e.target.value
- if (term !== "") {
- searchContents(term)
- .then((res) => res.results.map(entry => ({
- url: entry.object.properties.url,
- content: entry.snippet,
- title: entry.object.metadata.title
- })
- ))
- .then(results => displayResults(results))
- }
-}))
+registerHandlers(
+ debounce((e) => {
+ let term = e.target.value
+ if (term !== "") {
+ searchContents(term).then((results) => displayResults(results))
+ }
+ }),
+)
diff --git a/assets/styles/base.scss b/assets/styles/base.scss
index 80e29dec9..0696d2f4a 100644
--- a/assets/styles/base.scss
+++ b/assets/styles/base.scss
@@ -299,13 +299,11 @@ footer {
}
hr {
- width: 25%;
- margin: 4em auto;
- height: 2px;
- border-radius: 1px;
- border-width: 0;
- color: var(--dark);
- background-color: var(--dark);
+ width: 100%;
+ margin: 2em auto;
+ height: 1px;
+ border: none;
+ background-color: var(--outlinegray);
}
.page-end {
diff --git a/content/notes/config.md b/content/notes/config.md
index e9e769176..d6321b394 100644
--- a/content/notes/config.md
+++ b/content/notes/config.md
@@ -60,8 +60,10 @@ GitHubLink: https://github.com/jackyzha0/quartz/tree/hugo/content
# whether to use Operand to power semantic search
# IMPORTANT: replace this API key with your own if you plan on using
# Operand search!
-enableSemanticSearch: false
-operandApiKey: "REPLACE-WITH-YOUR-OPERAND-API-KEY"
+search:
+ enableSemanticSearch: false
+ operandApiKey: "REPLACE-WITH-YOUR-OPERAND-API-KEY"
+ operandIndexId: "REPLACE-WITH-YOUR-OPERAND-INDEX-ID"
# page description used for SEO
description:
diff --git a/content/notes/docker.md b/content/notes/docker.md
new file mode 100644
index 000000000..e96ede37c
--- /dev/null
+++ b/content/notes/docker.md
@@ -0,0 +1,56 @@
+---
+title: "Hosting with Docker"
+tags:
+- setup
+---
+
+If you want to host Quartz on a machine without using a webpage hosting service, it may be easier to [install Docker Compose](https://docs.docker.com/compose/install/) and follow the instructions below than to [install Quartz's dependencies manually](notes/preview%20changes.md).
+## Hosting Quartz Locally
+You can serve Quartz locally at `http://localhost:1313` with the following script, replacing `/path/to/quartz` with the
+actual path to your Quartz folder.
+
+docker-compose.yml
+```
+services:
+ quartz-hugo:
+ image: ghcr.io/jackyzha0/quartz:hugo
+ container_name: quartz-hugo
+ volumes:
+ - /path/to/quartz:/quartz
+ ports:
+ - 1313:1313
+
+ # optional
+ environment:
+ - HUGO_BIND=0.0.0.0
+ - HUGO_BASEURL=http://localhost
+ - HUGO_PORT=1313
+ - HUGO_APPENDPORT=true
+```
+
+Then run with: `docker-compose up -d` in the same directory as your `docker-compose.yml` file.
+
+While the container is running, you can update the `quartz` fork with: `docker exec -it quartz-hugo make update`.
+
+## Exposing Your Container to the Internet
+
+### To Your Public IP Address with Port Forwarding (insecure)
+
+Assuming you are already familiar with [port forwarding](https://en.wikipedia.org/wiki/Port_forwarding) and [setting it up with your router model](https://portforward.com):
+
+1. You should set the environment variable `HUGO_BASEURL=http://your-public-ip` and then start your container.
+2. Set up port forwarding on your router from port `p` to `your-local-ip:1313`.
+3. You should now be able to access Quartz from outside your local network at `http://your-public-ip:p`.
+
+However, your HTTP connection will be unencrypted and **this method is not secure**.
+
+### To a Domain using Cloudflare Proxy
+
+1. Port forward 443 (HTTPS) from your machine.
+2. Buy a custom domain (say, `your-domain.com`) from [Cloudflare](https://www.cloudflare.com/products/registrar/). Point a DNS A record from `your-domain.com` to your public IP address and enable the proxy.
+3. Set the environment variables `HUGO_BASEURL=https://your-domain.com`, `HUGO_PORT=443`, and `HUGO_APPENDPORT=false`. Change `1313:1313` to `443:443` for the `ports` in `docker-compose.yml`.
+4. Spin up your Quartz container and enjoy it at `https://your-domain.com`!
+
+### To a Domain using a Reverse Proxy
+
+If you want to serve more than just Quartz to the internet on this machine (or don't want to use the Cloudflare registrar and proxy), you should follow the steps in the section above (as appropriate) and also set up a reverse proxy, like [Traefik](https://doc.traefik.io/traefik). Be sure to configure your TLS certificates too!
diff --git a/content/notes/hosting.md b/content/notes/hosting.md
index fffcd653a..254d7af95 100644
--- a/content/notes/hosting.md
+++ b/content/notes/hosting.md
@@ -83,6 +83,10 @@ Only want to publish a subset of all of your notes? Don't worry, Quartz makes th
❌ [Excluding pages from being published](notes/ignore%20notes.md)
+## Docker Support
+If you don't want to use a hosting service, you can host using [Docker](notes/docker.md) instead!
+I would *not use this method* unless you know what you are doing.
+
---
Now that your Quartz is live, let's figure out how to make Quartz really *yours*!
diff --git a/content/notes/preview changes.md b/content/notes/preview changes.md
index 937b99bab..8ea07a391 100644
--- a/content/notes/preview changes.md
+++ b/content/notes/preview changes.md
@@ -5,9 +5,9 @@ tags:
weight: -2
---
-If you'd like to preview what your Quartz site looks like before deploying it to the internet, here's exactly how to do that!
+If you'd like to preview what your Quartz site looks like before deploying it to the internet, the following
+instructions guide you through installing the proper dependencies to run it locally.
-Note that both of these steps need to be completed.
## Install `hugo-obsidian`
This step will generate the list of backlinks for Hugo to parse. Ensure you have [Go](https://golang.org/doc/install) (>= 1.16) installed.
@@ -34,4 +34,10 @@ make serve
# View your site in a browser at http://localhost:1313/
```
-> 🌍 Step 5: [Hosting Quartz online!](notes/hosting.md)
\ No newline at end of file
+> [!INFO] Docker Support
+>
+> If you have Docker installed already, open your terminal, navigate to your folder with Quartz and run `make docker`
+
+Now that you are happy with how your Quartz instance looks, let's get it hosted!
+
+> 🌍 Step 5: [Hosting Quartz online!](notes/hosting.md)
diff --git a/content/notes/search.md b/content/notes/search.md
index ed170f64d..045769365 100644
--- a/content/notes/search.md
+++ b/content/notes/search.md
@@ -13,38 +13,25 @@ enableSemanticSearch: false
```
## Natural Language
-Natural language search is powered by [Operand](https://operand.ai/). It understands language like a person does and finds results that best match user intent. In this sense, it is closer to how Google Search works.
+Natural language search is powered by [Operand](https://beta.operand.ai/). It understands language like a person does and finds results that best match user intent. In this sense, it is closer to how Google Search works.
Natural language search tends to produce higher quality results than full-text search.
Here's how to set it up.
-1. Create an Operand Account on [their website](https://operand.ai/).
-2. Go to Dashboard > Settings > Integrations.
-3. Follow the steps to setup the GitHub integration. Operand needs access to GitHub in order to index your digital garden properly!
-4. Head over to Dashboard > Objects and press `(Cmd + K)` to open the omnibar and select 'Create Collection'.
- 1. Set the 'Collection Label' to something that will help you remember it.
- 2. You can leave the 'Parent Collection' field empty.
-5. Click into your newly made Collection.
- 1. Press the 'share' button that looks like three dots connected by lines.
- 2. Set the 'Interface Type' to `object-search` and click 'Create'.
- 3. This will bring you to a new page with a search bar. Ignore this for now.
-6. Go back to Dashboard > Settings > API Keys and find your Quartz-specific Operand API key under 'Other keys'.
- 1. Copy the key (which looks something like `0e733a7f-9b9c-48c6-9691-b54fa1c8b910`).
- 2. Open `data/config.yaml`. Set `enableSemanticSearch` to `true` and `operandApiKey` to your copied key.
+1. Login or Register for a new Operand account. Click the verification link sent to your email, and you'll be redirected to the dashboard. (Note) You do not need to enter a credit card to create an account, or get started with the Operand API. The first $10 of usage each month is free. To learn more, see pricing. If you go over your free quota, we'll (politely) reach out and ask you to configure billing.
+2. Create your first index. On the dashboard, under "Indexes", enter the name and description of your index, and click "Create Index". Note down the ID of the index (obtained by clicking on the index name in the list of indexes), as you'll need it in the next step. IDs are unique to each index, and look something like `uqv1duxxbdxu`.
+3. Click into the index you've created. Under "Index Something", select "SITEMAP" from the dropdown and click "Add Source".
+4. For the "Sitemap.xml URL", put your deployed site's base URL followed by `sitemap.xml`. For example, for `quartz.jzhao.xyz`, put `https://quartz.jzhao.xyz/sitemap.xml`. Leave the URL Regex empty.
+5. Get your API key. On the dashboard, under "API Keys", you can manage your API keys. If you don't already have an API key, click "Create API Key". You'll need this for the next step.
+6. Open `data/config.yaml`. Set `enableSemanticSearch` to `true`, `operandApiKey` to your copied key, and `operandIndexId` to the ID of the index we created from earlier..
```yaml {title="data/config.yaml"}
# the default option
-enableSemanticSearch: true
-operandApiKey: "0e733a7f-9b9c-48c6-9691-b54fa1c8b910"
+search:
+ enableSemanticSearch: true
+ operandApiKey: "jp9k5hudse2a828z98kxd6z3payi8u90rnjf"
+ operandIndexId: "s0kf3bd6tldw"
```
-7. Make a commit and push your changes to GitHub. See the [[notes/hosting|hosting]] page if you haven't done this already.
- 1. This step is *required* for Operand to be able to properly index your content.
- 2. Head over to Dashboard > Objects and select the collection that you made earlier
-8. Press `(Cmd + K)` to open the omnibar again and select 'Create GitHub Repo'
- 1. Set the 'Repository Label' to `Quartz`
- 2. Set the 'Repository Owner' to your GitHub username
- 3. Set the 'Repository Ref' to `master`
- 4. Set the 'Repository Name' to the name of your repository (usually just `quartz` if you forked the repository without changing the name)
- 5. Leave 'Root Path' and 'Root URL' empty
-9. Wait for your repository to index and enjoy natural language search in Quartz! Operand refreshes the index every 2h so all you need to do is just push to GitHub to update the contents in the search.
\ No newline at end of file
+7. Push your changes to the site and wait for it to deploy.
+8. Check the Operand dashboard and wait for your site to index. Enjoy natural language search powered by Operand!
diff --git a/data/config.yaml b/data/config.yaml
index c1230c0dd..6e1c75cc2 100644
--- a/data/config.yaml
+++ b/data/config.yaml
@@ -12,8 +12,10 @@ enableContextualBacklinks: true
enableRecentNotes: false
enableGitHubEdit: true
GitHubLink: https://github.com/jackyzha0/quartz/tree/hugo/content
-enableSemanticSearch: false
-operandApiKey: "REPLACE-WITH-YOUR-OPERAND-API-KEY"
+search:
+ enableSemanticSearch: false
+ operandApiKey: "REPLACE-WITH-YOUR-OPERAND-API-KEY"
+ operandIndexId: "REPLACE-WITH-YOUR-OPERAND-INDEX-ID"
description:
Host your second brain and digital garden for free. Quartz features extremely fast full-text search,
Wikilink support, backlinks, local graph, tags, and link previews.
diff --git a/layouts/partials/search.html b/layouts/partials/search.html
index c169289fe..b5692e18a 100644
--- a/layouts/partials/search.html
+++ b/layouts/partials/search.html
@@ -6,9 +6,9 @@
-{{if $.Site.Data.config.enableSemanticSearch}}
+{{if $.Site.Data.config.search.enableSemanticSearch}}
{{ $js := resources.Get "js/semantic-search.js" | resources.ExecuteAsTemplate "js/semantic-search.js" . | resources.Fingerprint "md5" | resources.Minify }}
-
+
{{else}}