Write your page
A frwrd.to page is one Markdown document: a short header with settings, then the text. You can write it by hand, in the web app, or let an assistant write it from this page. Every link on it becomes a short link with its own click numbers.
What a page is
A page is for the one link people tap in your profile: a logo, a name, a few buttons and your social icons. It has no custom code, no columns and no forms. What it can show is listed on this page, and nothing else is needed to make it look like yours: a logo, a colour, a button style, a shape and a font.
Where it lives
Your main page is at https://frwrd.to/@handle, where handle is your
workspace's id (it starts with w-), a handle you claimed, or your domain.
Further pages sit under it: https://frwrd.to/@handle/jobs is the page
jobs. In the API, the MCP tools and the command-line tool the main page is
called -.
A page in another language than your workspace's sits under that language's
code: https://frwrd.to/@handle/de/jobs. Pair the versions with
translations (below) and the page shows a language switcher.
Add .md to any page's address to read it as Markdown, for example
https://frwrd.to/@frwrd.to/help-pages.md is this page.
The header
The header is the first block of the document, between two lines that hold
only ---. It is YAML. Always put colours in quotes: without them # starts a
comment, so the page is refused until you add them.
| Field | What it is | Allowed values |
|---|---|---|
title | Required. The page's name, the browser tab and link previews | 1 to 120 characters |
description | One sentence for search results and link previews | up to 300 characters |
avatar | Your logo or photo, shown above the text. A web address, never a file | https address, no user name or password |
lang | The page's language. Leave it out: it follows the page's address | a language code such as en or de-AT; must match the address |
translations | The same page in other languages | { de: de/jobs }: language code, then the page's address; "-" for a main page |
utm | Tracking parameters added to every link on the page | any of source, medium, campaign, term, content, each 1 to 100 characters without spaces |
social | Your profiles, shown as a row of icons under the logo | a list of up to 12 https addresses |
theme.accent | The colour of links, icons, outlines and filled buttons | "#rrggbb"; must read on the background (see the contrast rule) |
theme.background | The page's background | light, dark, or "#rrggbb" |
theme.button | How link buttons look | outline (an outline in the accent, the default) or filled (the accent as background) |
theme.shape | The corners of buttons and icons | rounded (the default), pill or square |
theme.font | The typeface | sans (the default), serif or mono. System fonts only: nothing is loaded from elsewhere |
campaigns | Named time windows for timed blocks (below) | { black-friday: { from: 2026-11-27, until: 2026-11-30 } }; names of a-z, 0-9 and -; dates in UTC |
Anything else in the header is refused with the name of the key.
Everything under theme is optional. A page that sets nothing there looks the
same as it always did: a light background, a blue accent, outlined buttons
with rounded corners and a sans serif font.
Social icons
social lists full https addresses. frwrd.to recognises the network from
the address and draws its icon: Instagram, LinkedIn, X (also twitter.com),
YouTube, TikTok, GitHub, Facebook, Threads, Bluesky, Mastodon, Twitch,
Pinterest, Spotify, SoundCloud, Behance, Dribbble, Medium, Substack, WhatsApp
(wa.me), Telegram (t.me) and Discord. A Mastodon server is recognised by
its /@name path. Anything else gets a globe icon, named after its host.
social:
- https://www.instagram.com/milafischer
- https://www.linkedin.com/in/milafischer
- https://milafischer.example
Each icon is a short link like every other link: its clicks are counted and
the page's utm applies. The icons are in your accent colour, never the
network's brand colours, so they stay readable on your background. Each has an
accessible name, the network's. An e-mail address or a mailto: link is not
possible (only http and https addresses are links); write the address as
text in the body.
Social profiles come after the page's own links in the page's numbers, in the order you wrote them. A page can have 200 links in all, social ones included.
The contrast rule
A page whose text cannot be read is refused when you save it, with a message that names the fields and the measured ratio, for example:
theme.accent #ff5a1f on theme.background light (#ffffff) measures 3.11:1; links in the accent colour need at least 4.5:1 (WCAG AA). Pick a darker accent for a light background, a lighter one for a dark background, or change theme.background
You choose the background and the accent. frwrd.to works out the rest: the text colour is the near black or near white that reads best on your background, cards and secondary text are derived from it, and the label on a filled button is black or white, whichever reads better on your accent. Those always pass. What you can get wrong is the accent, so the rule is about it:
- The accent must have a contrast ratio of at least 4.5 to 1 against the background (WCAG 2.1 AA for text: links are drawn in the accent).
- It must also reach 3 to 1 against the cards, the tinted boxes behind code and the social icons. The message says which of the two failed.
In short: on a light background pick a dark accent, on a dark background a light one. Accents that work:
| Background | Accents that read |
|---|---|
light (white) | "#1f6feb", "#c2410c", "#0f766e", "#7c3aed" |
dark | "#ff7a59", "#7fb2ff", "#4ade80", "#fbbf24" |
"#fff8e7" (cream) | "#8a3b00", "#0f766e" |
"#10243e" (navy) | "#7fb2ff", "#fbbf24" |
A page you saved before this rule existed keeps working as it is; saving it again asks for a colour that reads.
The body
Everything after the header is Markdown, a safe subset of it.
| You write | You get |
|---|---|
# Name | The page's heading. ## and ### are smaller headings; deeper levels count as ### |
| a link alone on its line, or alone in a list item | A button: - [Book a call](https://example.com/call) |
| a link inside a sentence | An ordinary link: See [the docs](https://example.com/docs). |
<https://example.com> or a bare https://example.com | A link with its own address as text |
**bold** and *emphasis* | Bold and italic |
a list with - or 1. | A list; lists and quotes nest up to 12 levels |
`code` and fenced blocks | Inline code and code blocks; line breaks are kept |
> text | A quote |
| a GFM table: a header row, a row of dashes under it, then the rows | A table that scrolls sideways when it is wide |
 | An image, https only. Always write the alt text |
--- alone on a line | A dividing line |
Raw HTML is dropped. A link whose address is not http or https stays
text, and so do www. addresses and e-mail addresses written without a
scheme. A link to another page of yours starts with /@handle/: it is counted
like every link, without tracking parameters.
A page can be 64 KiB at most. Links are checked when you save: an address
that points inside a network (such as localhost) is refused, and so is one
with a user name and password in it.
Timed blocks
A block can show only during a campaign. Put it between a line that says when
and a line that holds only ::::
::: from 2026-11-27 until 2026-11-30
- [Black Friday: 30 % off](https://example.com/sale)
:::
Either end can be left out: ::: from 2026-12-24 shows from then on,
::: until 2026-12-31 until then. For a campaign that appears in several
places, name it once in the header and use the name:
---
title: Acme
campaigns:
black-friday: { from: 2026-11-27, until: 2026-11-30 }
---
::: black-friday
- [Shop the sale](https://example.com/sale)
:::
::: not black-friday
- [Shop](https://example.com/shop)
:::
::: not black-friday shows whenever the campaign is not running, so the
regular button comes back by itself when the sale ends.
All times are UTC. A date means the whole day in UTC:
until 2026-11-30 shows the block through the end of November 30, UTC. For
a time of day, write it in UTC with a Z: from 2026-11-27T08:00Z is 9:00
in Vienna in winter. A time with another zone, such as +01:00, is refused.
The links in a block work all the time, also before the block shows and after it ended. Every link gets its short link when you save, so you can test the campaign's links, QR codes and tracking before it starts.
Blocks do not nest, a page holds at most 20, and the ::: lines start at the
beginning of the line. Before a block shows, the page's Markdown version
(.md) leaves it out, and the campaign names in the header too.
Check before you save
The web app's editor has a Check button, the API
POST /v1/pages/{path}/check, MCP the tool check_page and the
command-line tool frwrd page check <path> < page.md. Each runs every check a
save runs, saves nothing, and lists warnings: things that are allowed but
probably not meant, such as a missing description, a campaign no block uses,
a timed block that has already ended, a ::: that starts with spaces (so it
is shown as text), a skipped heading level, a second # title, an http
link or an image without a description. Warnings never stop a save; errors
do, and they say what to fix.
Three complete examples
Each one is a whole document, ready to paste. Replace the addresses with yours.
A link in bio
---
title: Mila Fischer
description: Illustrator in Vienna. Commissions open.
avatar: https://example.com/mila.jpg
social:
- https://www.instagram.com/milafischer
- https://www.behance.net/milafischer
- https://www.youtube.com/@milafischer
theme: { background: "#fff8e7", accent: "#8a3b00", button: filled, shape: pill, font: serif }
utm: { source: bio, medium: social }
---
# Mila Fischer
Illustrator in Vienna. I draw books, posters and the odd mural.
- [Book a commission](https://example.com/commissions)
- [Shop prints](https://example.com/shop)
- [Newsletter, once a month](https://example.com/newsletter)
An event
---
title: Frwrd Summit 2027
description: Two days on links, pages and numbers. Vienna, 14 and 15 May 2027.
social: [https://www.linkedin.com/company/example, https://bsky.app/profile/example.com]
theme: { background: dark, accent: "#7fb2ff", button: outline, shape: square, font: sans }
utm: { source: summit, medium: qr, campaign: summit-2027 }
---
# Frwrd Summit 2027
**14 and 15 May 2027**, Museumsquartier, Vienna.
- [Get your ticket](https://example.com/tickets)
- [See the programme](https://example.com/programme)
- [Become a speaker](https://example.com/speakers)
## Good to know
> Doors open at 08:30. Lunch is included.
| Day | Topic |
|:--|:--|
| 14 May | Links and pages |
| 15 May | Numbers |
Getting there: [the venue on the map](https://example.com/map).
A portfolio
---
title: Jonas Weber, product designer
description: Selected work, writing and contact.
avatar: https://example.com/jonas.jpg
social: [https://github.com/jonasweber, https://dribbble.com/jonasweber, https://www.linkedin.com/in/jonasweber]
theme: { background: light, accent: "#0f766e", button: outline, shape: rounded, font: mono }
---
# Jonas Weber
Product designer. I make checkout flows less painful.
## Selected work
- [Checkout redesign for Acme](https://example.com/work/acme)
- [A design system for Beta](https://example.com/work/beta)
## Writing
My latest piece is [Why buttons need labels](https://example.com/blog/buttons).
All of it is at https://example.com/blog.
## Contact
Write to hello at example dot com, or use [the contact form](https://example.com/contact).
Let an AI write it
Copy this prompt into any assistant (Claude, ChatGPT or another one that can read a web address). It reads this page, asks you what it needs and gives you the finished document.
Read https://frwrd.to/@frwrd.to/help-pages.md. It describes the Markdown format of a frwrd.to page: the header (title, description, avatar, social, theme) and the body.
Ask me, one question at a time:
1. My brand or name.
2. One sentence about what I do.
3. The links I want on the page, each with a label.
4. My social profiles, as full https addresses.
5. The address of my logo.
6. My colours: a background (light, dark or a #rrggbb colour) and an accent (#rrggbb).
Then write the whole page as one Markdown document, header included, in a single code block. Follow the format exactly. Check the contrast rule: the accent needs a contrast ratio of at least 4.5 to 1 against the background, so on a light background use a dark accent and on a dark background a light one. Put every colour in quotes. Do not invent links, profiles or facts. Use the three examples on that page as a guide for the structure.
Then save the result: paste it into the page editor in the
web app, or let your assistant publish it through
MCP or the command-line tool
(frwrd page push). The save tells you exactly what to fix if something in
the document is not allowed.