Manage and surface technical documentation in Port
This guide demonstrates how to bring a TechDocs experience into Port, so your markdown documentation lives alongside the rest of your software catalog.
Three ideas drive the implementation of this guide:
-
Documentation lives as entities.
Port persists each markdown file as atechDocentity in your software catalog. Once data is in the catalog, every Port surface (search, AI, dashboards, plugins) can reach it through the same blueprint and relations. -
Ingestion is source-agnostic.
This guide uses GitHub as the example, but the same data model works with any source that can fetch markdown and upsert it into Port: GitLab, Bitbucket, Azure DevOps, Confluence, a webhook, or a CI job that calls Port's upsert entity API. Pick the source you have, keep thetechDocshape, and every downstream visualization keeps working unchanged. -
Visualization follows context.
The TechDocs plugin and the built-in markdown widget scope what they render based on the page they sit on. When placed on a service entity page, they show that service's documents; when placed on a repository entity page, they show that repository's documents; on a global dashboard, they show everything. The scoping uses the relations you define on thetechDocblueprint.
Common use cases
- Centralize technical documentation from many repositories into a single, queryable catalog.
- Display documentation next to the service or repository it describes, on the relevant entity page.
- Surface documentation in Port's global search and Port AI for fast, contextual answers.
- Track ownership and freshness of documentation alongside the rest of your software catalog.
- Optionally give your team a navigable in-Port reading experience for documentation, with a folder-based sidebar and document metadata.
- Optionally let teammates discuss documents and resolve threads alongside the content.
Prerequisites
This guide assumes the following:
- You have a Port account and have completed the onboarding process.
- One of Port's Git integrations is installed in your organization.
Set up data model
- If you completed Port's onboarding, you already have a
serviceblueprint with a relation togithubRepository. In this case, relatetechDocdirectly toserviceso documents follow the service they describe. The plugin can still scope by repository through the existing service-to-repository link. - If you do not have a
serviceblueprint, or it is not connected to your repository blueprint, relatetechDocdirectly togithubRepository. This is the right choice for legacy catalogs where services were never modeled.
Choose the case that matches your catalog and follow the matching tab below. The rest of the guide carries the same choice through the integration mapping and the plugin parameters.
- Connected to a service blueprint
- Connected to a repository blueprint
Create the techdoc blueprint with a service relation
-
Go to the Builder page of your portal.
-
Click on
+ Blueprint. -
Click on the
{...} Edit JSONbutton. -
Copy and paste the following JSON schema:
Tech Doc blueprint (click to expand)
{"identifier": "techDoc","title": "Tech Doc","icon": "Book","schema": {"properties": {"content": {"title": "Content","type": "string","format": "markdown","description": "The raw markdown content of the document"},"filePath": {"title": "File Path","type": "string","description": "Path to the file within the repository"},"folderPath": {"title": "Folder Path","type": "string","description": "Parent folder path (e.g. apps/Frontend)"},"url": {"title": "GitHub URL","type": "string","format": "url","description": "Direct link to the file on GitHub"},"lastUpdated": {"title": "Last Updated","type": "string","format": "date-time"},"archived": {"type": "boolean","title": "Archived","default": false}},"required": ["content"]},"mirrorProperties": {},"calculationProperties": {},"aggregationProperties": {},"relations": {"service": {"title": "Service","target": "service","required": true,"many": false}}} -
Click
Createto save the blueprint.
Create the techdoc blueprint with a repository relation
-
Go to the Builder page of your portal.
-
Click on
+ Blueprint. -
Click on the
{...} Edit JSONbutton. -
Copy and paste the following JSON schema based on your Git provider:
- GitHub
- GitLab
- Azure DevOps
Tech Doc blueprint for GitHub (click to expand)
{"identifier": "techDoc","title": "Tech Doc","icon": "Book","schema": {"properties": {"content": {"title": "Content","type": "string","format": "markdown","description": "The raw markdown content of the document"},"filePath": {"title": "File Path","type": "string","description": "Path to the file within the repository"},"folderPath": {"title": "Folder Path","type": "string","description": "Parent folder path (e.g. apps/Frontend)"},"url": {"title": "URL","type": "string","format": "url","description": "Direct link to the file in the repository"},"lastUpdated": {"title": "Last Updated","type": "string","format": "date-time"},"archived": {"type": "boolean","title": "Archived","default": false}},"required": ["content"]},"mirrorProperties": {},"calculationProperties": {},"aggregationProperties": {},"relations": {"repository": {"title": "Repository","target": "githubRepository","required": true,"many": false}}}Tech Doc blueprint for GitLab (click to expand)
{"identifier": "techDoc","title": "Tech Doc","icon": "Book","schema": {"properties": {"content": {"title": "Content","type": "string","format": "markdown","description": "The raw markdown content of the document"},"filePath": {"title": "File Path","type": "string","description": "Path to the file within the repository"},"folderPath": {"title": "Folder Path","type": "string","description": "Parent folder path (e.g. apps/Frontend)"},"url": {"title": "URL","type": "string","format": "url","description": "Direct link to the file in the repository"},"lastUpdated": {"title": "Last Updated","type": "string","format": "date-time"},"archived": {"type": "boolean","title": "Archived","default": false}},"required": ["content"]},"mirrorProperties": {},"calculationProperties": {},"aggregationProperties": {},"relations": {"repository": {"title": "Repository","target": "gitlabRepository","required": true,"many": false}}}Tech Doc blueprint for Azure DevOps (click to expand)
{"identifier": "techDoc","title": "Tech Doc","icon": "Book","schema": {"properties": {"content": {"title": "Content","type": "string","format": "markdown","description": "The raw markdown content of the document"},"filePath": {"title": "File Path","type": "string","description": "Path to the file within the repository"},"folderPath": {"title": "Folder Path","type": "string","description": "Parent folder path (e.g. apps/Frontend)"},"url": {"title": "URL","type": "string","format": "url","description": "Direct link to the file in the repository"},"lastUpdated": {"title": "Last Updated","type": "string","format": "date-time"},"archived": {"type": "boolean","title": "Archived","default": false}},"required": ["content"]},"mirrorProperties": {},"calculationProperties": {},"aggregationProperties": {},"relations": {"repository": {"title": "Repository","target": "azureDevopsRepository","required": true,"many": false}}} -
Click
Createto save the blueprint.
The example above relates techDoc to the git source you selected, which is the default blueprint created by Port's GitHub integration. If your repository blueprint uses a different identifier (for example, when ingesting from GitLab or another provider), change the target value accordingly.
Update integration mapping
Next, we will configure your Git integration to fetch markdown files from your repositories and upsert them as techDoc entities. The integration uses the file kind, which fetches file contents and exposes them in your JQ mappings.
The mapping below ingests markdown files from a list of repositories you specify. We start narrow on purpose so you can confirm the pipeline end-to-end with a couple of known repos, then broaden the file selection (more repos, docs/ folders, other markdown sources) once everything looks right.
-
Go to your data sources page and click on your Git integration.
-
Open the Mapping tab.
-
Click on the
{...} Edit YAMLbutton. -
Append the following block to your existing configuration:
- Connected to a service blueprint
- Connected to a repository blueprint
- GitHub
- GitLab
- Azure DevOps
This guide includes steps that require integration with GitHub:
- GitHub (Ocean) - uses the Ocean framework. We strongly recommend this integration for new and migrated setups.
- GitHub (Sunset) - uses a GitHub app that is in sunset and will be fully deprecated on September 15, 2026.
- GitHub (Ocean)
- GitHub (Sunset)
Tech Doc mapping for GitHub Ocean (click to expand)
resources:
- kind: file
selector:
query: '.path | startswith("node_modules/") | not'
files:
- path: '**/*.md'
repos:
- name: my-service-repo # Replace with your repository name
branch: main
- name: my-other-service-repo # Replace with your repository name
branch: main
skipParsing: true
port:
entity:
mappings:
identifier: '.repository.name + "-" + (.path | gsub("/"; "-") | gsub("\\."; "-") | gsub(" ";"-"))'
title: '.path | split("/") | .[-1] | split(".") | .[0]'
blueprint: '"techDoc"'
properties:
content: .content
filePath: .path
folderPath: .path | split("/") | .[:-1] | join("/")
url: '.repository.html_url + "/blob/" + .repository.default_branch + "/" + .path'
lastUpdated: >-
(try (.commit.commit.committer.date // .commit.commit.author.date //
.commit.committer.date // .commit.author.date) catch null)
// .repository.pushed_at // .repository.updated_at
relations:
service: .repository.name
Tech Doc mapping for GitHub Sunset (click to expand)
resources:
- kind: file
selector:
query: '(.file.path | startswith("node_modules/")) | not'
files:
- path: '**/*.md'
repos:
- my-service-repo # Replace with your repository name
- my-other-service-repo # Replace with your repository name
skipParsing: true
port:
entity:
mappings:
identifier: '.repo.name + "-" + (.file.path | gsub("/"; "-") | gsub("\\."; "-") | gsub(" ";"-"))'
title: '.file.path | split("/") | .[-1] | split(".") | .[0]'
blueprint: '"techDoc"'
properties:
content: .file.content
filePath: .file.path
folderPath: .file.path | split("/") | .[:-1] | join("/")
url: '.repo.html_url + "/blob/" + .repo.default_branch + "/" + .file.path'
lastUpdated: .repo.pushed_at
relations:
service: .repo.name
Tech Doc mapping for GitLab (click to expand)
resources:
- kind: file
selector:
query: 'true'
files:
path: '*.md'
repos:
- group/my-service-repo # Replace with your group/project path
- group/my-other-service-repo # Replace with your group/project path
port:
entity:
mappings:
identifier: (.repo.path_with_namespace | gsub("/"; "-")) + "-" + (.file.file_path | gsub("/"; "-") | gsub("[.]"; "-"))
title: .file.file_name | split(".") | .[0]
blueprint: '"techDoc"'
properties:
content: .file.content
filePath: .file.file_path
folderPath: .file.file_path | split("/") | .[:-1] | join("/")
url: .repo.web_url + "/-/blob/" + .repo.default_branch + "/" + .file.file_path
relations:
service: .repo.path_with_namespace
GitLab's file kind uses simple wildcards (e.g. *.md) rather than recursive glob patterns like **/*.md. For more details, see the GitLab capabilities documentation.
Tech Doc mapping for Azure DevOps (click to expand)
resources:
- kind: file
selector:
query: 'true'
files:
path: '**/*.md'
repos:
- my-service-repo # Replace with your repository name
- my-other-service-repo # Replace with your repository name
port:
entity:
mappings:
identifier: .repo.name + "-" + (.file.path | gsub("/"; "-") | gsub("[.]"; "-"))
title: .file.path | split("/") | .[-1] | split(".") | .[0]
blueprint: '"techDoc"'
properties:
content: .file.content
filePath: .file.path
folderPath: .file.path | split("/") | .[:-1] | join("/")
url: .repo.url + "?path=" + .file.path
relations:
service: .repo.name
- GitHub
- GitLab
- Azure DevOps
- GitHub (Ocean)
- GitHub (Sunset)
Tech Doc mapping for GitHub Ocean (click to expand)
resources:
- kind: file
selector:
query: '.path | startswith("node_modules/") | not'
files:
- path: '**/*.md'
repos:
- name: my-service-repo
branch: main
- name: my-other-service-repo
branch: main
skipParsing: true
port:
entity:
mappings:
identifier: '.repository.name + "-" + (.path | gsub("/"; "-") | gsub("\\."; "-"))'
title: '.path | split("/") | .[-1] | split(".") | .[0]'
blueprint: '"techDoc"'
properties:
content: .content
filePath: .path
folderPath: .path | split("/") | .[:-1] | join("/")
url: '.repository.html_url + "/blob/" + .repository.default_branch + "/" + .path'
lastUpdated: >-
(try (.commit.commit.committer.date // .commit.commit.author.date //
.commit.committer.date // .commit.author.date) catch null)
// .repository.pushed_at // .repository.updated_at
relations:
repository: .repository.name
Tech Doc mapping for GitHub Sunset (click to expand)
resources:
- kind: file
selector:
query: '(.file.path | startswith("node_modules/")) | not'
files:
- path: '**/*.md'
repos:
- my-service-repo
- my-other-service-repo
skipParsing: true
port:
entity:
mappings:
identifier: '.repo.name + "-" + (.file.path | gsub("/"; "-") | gsub("\\."; "-"))'
title: '.file.path | split("/") | .[-1] | split(".") | .[0]'
blueprint: '"techDoc"'
properties:
content: .file.content
filePath: .file.path
folderPath: .file.path | split("/") | .[:-1] | join("/")
url: '.repo.html_url + "/blob/" + .repo.default_branch + "/" + .file.path'
lastUpdated: .repo.pushed_at
relations:
repository: .repo.name
Tech Doc mapping for GitLab (click to expand)
resources:
- kind: file
selector:
query: 'true'
files:
path: '*.md'
repos:
- group/my-service-repo # Replace with your group/project path
- group/my-other-service-repo # Replace with your group/project path
port:
entity:
mappings:
identifier: (.repo.path_with_namespace | gsub("/"; "-")) + "-" + (.file.file_path | gsub("/"; "-") | gsub("[.]"; "-"))
title: .file.file_name | split(".") | .[0]
blueprint: '"techDoc"'
properties:
content: .file.content
filePath: .file.file_path
folderPath: .file.file_path | split("/") | .[:-1] | join("/")
url: .repo.web_url + "/-/blob/" + .repo.default_branch + "/" + .file.file_path
relations:
repository: .repo.path_with_namespace
GitLab's file kind uses simple wildcards (e.g. *.md) rather than recursive glob patterns like **/*.md. For more details, see the GitLab capabilities documentation.
Tech Doc mapping for Azure DevOps (click to expand)
resources:
- kind: file
selector:
query: 'true'
files:
path: '**/*.md'
repos:
- my-service-repo # Replace with your repository name
- my-other-service-repo # Replace with your repository name
port:
entity:
mappings:
identifier: .repo.name + "-" + (.file.path | gsub("/"; "-") | gsub("[.]"; "-"))
title: .file.path | split("/") | .[-1] | split(".") | .[0]
blueprint: '"techDoc"'
properties:
content: .file.content
filePath: .file.path
folderPath: .file.path | split("/") | .[:-1] | join("/")
url: .repo.url + "?path=" + .file.path
relations:
repository: .repo.name
- Click
Save & Resyncto apply the mapping and trigger a sync.
After the sync completes, open your catalog and switch to the Tech Doc tab to confirm that documents were ingested.
The title field is a JQ expression, so you can shape entity titles to suit how you browse the catalog (for example, include the repository name or the full path) when the default produces collisions like several README entries.
Refine the file selection
Once the initial sync looks right, you can broaden or tighten the selection in two ways:
-
Glob patterns and repository scope control which files are fetched. The syntax differs by provider:
- GitHub: Use
files:as an array (- path:). Path supports full glob patterns (**/*.md).reposis an array of objects withnameandbranch. Omitreposto scan all accessible repositories. - GitLab: Use
files:as an object (path:). Path supports simple wildcards (*.md), not full glob syntax.reposis a list ofgroup/projectstrings. - Azure DevOps: Use
files:as an object (path:). Path supports full glob patterns (**/*.md).reposis an array of repository name strings.
- GitHub: Use
-
JQ filters in
selector.querycontrol which fetched files become entities. The field paths differ by provider:
- GitHub
- GitLab
- Azure DevOps
- GitHub Ocean
- GitHub (Sunset)
# Exclude node_modules and files whose name starts with an underscore.
query: '(.path | startswith("node_modules/") | not) and (.name | startswith("_") | not)'
# Only include files under a top-level "docs" folder.
query: '.path | startswith("docs/")'
# Regex match on file name (using JQ's test function).
query: '.name | test("^(README|CHANGELOG|guide-.*)\\.md$")'
# Exclude node_modules and files whose name starts with an underscore.
query: '(.file.path | startswith("node_modules/") | not) and (.file.name | startswith("_") | not)'
# Only include files under a top-level "docs" folder.
query: '.file.path | startswith("docs/")'
# Regex match on file name (using JQ's test function).
query: '.file.name | test("^(README|CHANGELOG|guide-.*)\\.md$")'
# Exclude node_modules and files whose name starts with an underscore.
query: '(.file.file_path | startswith("node_modules/") | not) and (.file.file_name | startswith("_") | not)'
# Only include files under a top-level "docs" folder.
query: '.file.file_path | startswith("docs/")'
# Regex match on file name (using JQ's test function).
query: '.file.file_name | test("^(README|CHANGELOG|guide-.*)\\.md$")'
# Exclude node_modules and files whose name starts with an underscore.
query: '(.file.path | startswith("node_modules/") | not) and (.file.fileName | startswith("_") | not)'
# Only include files under a top-level "docs" folder.
query: '.file.path | startswith("docs/")'
# Regex match on file name (using JQ's test function).
query: '.file.fileName | test("^(README|CHANGELOG|guide-.*)\\.md$")'
Add the TechDocs plugin (optional)
The built-in markdown widget displays a single techDoc entity at a time. For a richer experience with a folder-based navigation across multiple documents, Port provides a reference TechDocs plugin built on the plugins framework. You can use this plugin as-is, customize it to fit your needs, or build your own visualization from scratch.
The plugin reads the page it sits on and scopes the document list automatically, so the same plugin works for both data-model cases above:
- On a service entity page, it shows only the documents related to that service.
- On a repository entity page, it shows only the documents related to that repository.
- On a dashboard, it is not scoped to any single entity and displays every
techDocin the catalog (across all services and repositories).
The TechDocs plugin does not render images embedded in markdown.
Set up the plugin
-
Clone Port's Plugin repository.
-
Enter the
techdocsfolder. -
Build the plugin and upload it with the Port plugins CLI by running the following commands:
npm installnpm run buildport-plugins upload \--file dist/index.html \--identifier techdocs-port-plugin \--title "TechDocs Viewer" \--params "$(cat upload-params.json)" \--upsert -
On a dashboard or entity page, click
+ Widget, select Custom, choose the TechDocs Viewer plugin. -
Type
TechDocs Viewerin theTitlefield. -
Select
TechDocas theTechDoc blueprintparameter. -
Select
GitHub RepositoryorServiceas theTech Doc related blueprintparameter depending on your data model. -
Click
Createto save the plugin.
Use Port AI and search to find documentation
Once documents live in the catalog, they are automatically available everywhere else in Port:
- Global search: type a phrase from any document into Port's global search (
Ctrl + KorCommand + K) to find matchingtechDocentities. See global search for query syntax. - API: read documents programmatically with the
POST /v1/blueprints/techDoc/entities/searchendpoint. See the search entities API reference. - Port AI: open the AI assistant with
Ctrl + I(orCommand + I) and ask natural-language questions such as:- "What tech stack was used in x service?"
- "Which services are missing documentation in our catalog?"
- "Where do we describe how to rotate the auth service's signing keys?"
Conclusion
You now have a complete pipeline for managing technical documentation in Port:
- A
techDocblueprint that captures markdown content, file context, and a link back to either the service or the repository it belongs to, depending on your data model. - A GitHub integration mapping that keeps documentation in sync with its source of truth.
- Built-in widgets and a dashboard that expose documentation across the catalog.
- An optional plugin for a richer, navigable reading experience that scopes itself to the current service or repository entity page.
- Out-of-the-box availability in global search, the Port API, and Port AI.
Because the contract is on the techDoc shape, you can extend this pattern to additional sources (GitLab, Bitbucket, Confluence, a webhook, or a CI job) without changing the dashboards or plugin layer.