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.
This guide includes one or more steps that require integration with GitHub.
Port supports two GitHub integrations:
- GitHub (Legacy) - uses a GitHub app, which is soon to be deprecated.
- GitHub (Ocean) - uses the Ocean framework, recommended for new integrations.
Both integration options are present in this guide via tabs, choose the one that fits your needs.
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 the following GitHub integrations is installed:
- GitHub (Ocean)
- GitHub (Legacy)
Port's GitHub Ocean integration is installed in your organization.
Port's GitHub app 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:
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": {"repository": {"title": "Repository","target": "githubRepository","required": true,"many": false}}} -
Click
Createto save the blueprint.
The example above relates techDoc to githubRepository, 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 the GitHub 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 each repository's top-level README.md 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 GitHub 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 (Ocean)
- GitHub (Legacy)
Tech Doc mapping for GitHub (Ocean) (click to expand)
resources:- kind: fileselector:query: '.path | startswith("node_modules/") | not'files:- path: '**/*.md'repos:- name: my-service-repo # Replace with your repository namebranch: main- name: my-other-service-repo # Replace with your repository namebranch: mainskipParsing: trueport:entity:mappings:identifier: '.repository.name + "-" + (.path | gsub("/"; "-") | gsub("\\."; "-") | gsub(" ";"-"))'title: '.path | split("/") | .[-1] | split(".") | .[0]'blueprint: '"techDoc"'properties:content: .contentfilePath: .pathfolderPath: .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_atrelations:service: .repository.nameTech Doc mapping for GitHub (Legacy) (click to expand)
resources:- kind: fileselector: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 nameskipParsing: trueport:entity:mappings:identifier: '.repo.name + "-" + (.file.path | gsub("/"; "-") | gsub("\\."; "-")) | gsub(" ";"-"))''title: '.file.path | split("/") | .[-1] | split(".") | .[0]'blueprint: '"techDoc"'properties:content: .file.contentfilePath: .file.pathfolderPath: .file.path | split("/") | .[:-1] | join("/")url: '.repo.html_url + "/blob/" + .repo.default_branch + "/" + .file.path'lastUpdated: .repo.pushed_atrelations:service: .repo.name- GitHub (Ocean)
- GitHub (Legacy)
Tech Doc mapping for GitHub (Ocean) (click to expand)
resources:- kind: fileselector:query: '.path | startswith("node_modules/") | not'files:- path: '**/*.md'repos:- name: my-service-repobranch: main- name: my-other-service-repobranch: mainskipParsing: trueport:entity:mappings:identifier: '.repository.name + "-" + (.path | gsub("/"; "-") | gsub("\\."; "-"))'title: '.path | split("/") | .[-1] | split(".") | .[0]'blueprint: '"techDoc"'properties:content: .contentfilePath: .pathfolderPath: .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_atrelations:repository: .repository.nameTech Doc mapping for GitHub (Legacy) (click to expand)
resources:- kind: fileselector:query: '(.file.path | startswith("node_modules/")) | not'files:- path: '**/*.md'repos:- my-service-repo- my-other-service-reposkipParsing: trueport:entity:mappings:identifier: '.repo.name + "-" + (.file.path | gsub("/"; "-") | gsub("\\."; "-"))'title: '.file.path | split("/") | .[-1] | split(".") | .[0]'blueprint: '"techDoc"'properties:content: .file.contentfilePath: .file.pathfolderPath: .file.path | split("/") | .[:-1] | join("/")url: '.repo.html_url + "/blob/" + .repo.default_branch + "/" + .file.path'lastUpdated: .repo.pushed_atrelations: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 in
files[]control which files are fetched. Add more entries (each with its ownpath,repos, andskipParsing) to ingest more sources. Examples:- Replace
**/*.mdwith**/docs/**/*.mdto ingest every markdown file under anydocs/folder in the listed repos. - Add a second
pathentry with a different repo list to ingest from another set of repositories. - Omit
reposentirely to scan every repository the integration has access to.
- Replace
-
JQ filters in
selector.querycontrol which fetched files become entities. The examples below use the Ocean field paths (.path,.name); for Legacy, prefix them with.file.(.file.path,.file.name).# 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$")'
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.