# HTML, CSS, & HTTP

## Getting Started

## Static sites

OpenGraph+ works best with dynamic frameworks like Rails, Django, or Next.js where meta tags are generated per-request. For static sites, you'll need to generate and paste meta tags for each page.

## Install the CLI

Install the OpenGraph+ CLI from your terminal:

```sh
curl -sSL https://ogplus.terminalwire.sh/ | bash
```

## Generate meta tags for each page

Run the CLI to generate meta tags for a page:

```sh
ogplus meta $OGPLUS__WEBSITE_HOST
```

Copy the output into your page's `<head>`:

```html
<html>
  <head>
    <meta property="og:title" content="...">
    <meta property="og:image" content="...">
    <!-- ... other tags -->
  </head>
</html>
```

Repeat for each page on your site:

```sh
# These pages might not exist on your site
ogplus meta $OGPLUS__WEBSITE_HOST/about
ogplus meta $OGPLUS__WEBSITE_HOST/contact
ogplus meta $OGPLUS__WEBSITE_HOST/pricing
ogplus meta $OGPLUS__WEBSITE_HOST/blog
ogplus meta $OGPLUS__WEBSITE_HOST/blog/hello-world
```

This manual process is required for static sites. For a better experience, consider using a dynamic framework like Rails, Django, or Next.js where meta tags are generated automatically per-request.

## Publish and verify

Publish your site, then use the preview tool to verify your social cards render correctly.


## Rendering

When a social platform requests your page's `og:image`, OpenGraph+ loads your page in a headless browser and captures it as an image.

## The render process

1. Social platform (LinkedIn, Twitter, etc.) requests your page
2. Your server returns HTML with `og:image` pointing to OpenGraph+
3. Platform fetches the image URL
4. OpenGraph+ loads your page in a headless browser
5. OpenGraph+ injects `data-ogplus` on `<html>`
6. OpenGraph+ checks for templates or applies CSS styling
7. OpenGraph+ captures the viewport as an image
8. Image is returned to the social platform

## The data attribute

When rendering, OpenGraph+ adds `data-ogplus` to your `<html>` element:

```html
<!-- Generic render -->
<html data-ogplus>

<!-- Platform-specific render -->
<html data-ogplus="linkedin">
<html data-ogplus="twitter">
```

This attribute is your hook for styling. Any CSS that targets `[data-ogplus]` only applies during social card rendering—not to regular visitors.

## Rendering modes

OpenGraph+ supports three rendering modes, checked in this order:

1. **Templates** — Custom layouts using `<template id="ogplus">` elements. See [Templates](/docs/html-css/templates).
2. **Selector isolation** — Extract a specific element using `og:plus:selector` meta tag.
3. **Full page** — Render the entire viewport with CSS styling via `data-ogplus`.

## Meta tags reference

Control rendering behavior directly in your HTML with meta tags.

| Meta tag | Description |
|----------|-------------|
| `og:plus:viewport:width` | Viewport width in pixels |
| `og:plus:selector` | CSS selector to isolate |
| `og:plus:style` | Inline CSS for body |
| `og:plus:cache:max_age` | Cache TTL in seconds |
| `og:plus:cache:etag` | Cache version identifier |

### Viewport width

Override the viewport width for this page:

```html
<meta property="og:plus:viewport:width" content="800">
```

Mobile widths (600-800px) tend to work best for social cards since they force a simpler, more focused layout.

### Selector

Capture only a specific element instead of the full viewport:

```html
<meta property="og:plus:selector" content="#social-card">
```

If the selector matches a `<template>` element, OpenGraph+ treats it as a template (evaluating `${...}` expressions). Otherwise, it clones the element and places it directly in `<body>` before capturing. Cloned elements may have rendering issues if they rely on inherited styles from parent elements or CSS that uses descendant selectors.

### Style

Inject inline styles during rendering:

```html
<meta property="og:plus:style" content="background: white; padding: 40px;">
```

These styles apply to the `<body>` element during capture. Use this for quick adjustments without modifying your CSS.

### Cache max age

Set how long the rendered image stays fresh (in seconds):

```html
<meta property="og:plus:cache:max_age" content="86400">
```

This overrides HTTP `Cache-Control` headers. See [HTTP Caching](/docs/html-css/caching) for details.

### Cache etag

Set a version identifier for cache invalidation:

```html
<meta property="og:plus:cache:etag" content="v1.2.3">
```

When the etag changes, OpenGraph+ re-renders even if the cache hasn't expired. See [HTTP Caching](/docs/html-css/caching) for details.

## Example

Combine meta tags to fully control rendering:

```html
<head>
  <meta property="og:plus:viewport:width" content="800">
  <meta property="og:plus:selector" content=".blog-hero">
  <meta property="og:plus:style" content="padding: 20px;">
  <meta property="og:plus:cache:max_age" content="86400">
</head>
```

This renders at 800px wide, extracts the `.blog-hero` element, adds padding around it, and caches the result for 24 hours.


## CSS Styling

## How it works

When OpenGraph+ renders your page as a social card, it adds a `data-ogplus` attribute to the root `<html>` element:

```html
<!-- Generic render -->
<html data-ogplus>

<!-- Platform-specific render -->
<html data-ogplus="twitter">
<html data-ogplus="linkedin">
```

This attribute only exists during screenshot capture—your regular visitors never see it. Use CSS selectors to target this attribute and style your page differently for social cards.

## Plain CSS

With CSS nesting, you can group all your social card styles in one block:

```css
html[data-ogplus] {
  nav { display: none; }
  footer { display: none; }
  .cookie-banner { display: none; }
  
  .hero {
    padding: 60px;
    font-size: 1.5rem;
  }
}
```

Target specific platforms:

```css
html[data-ogplus="twitter"] {
  .hero { background: #1da1f2; }
}

html[data-ogplus="linkedin"] {
  .hero { padding: 80px; }
}
```

### Show elements only in social cards

Hide content from regular visitors, reveal it in screenshots:

```css
.social-only {
  display: none;
}

html[data-ogplus] .social-only {
  display: block;
}
```

### Without nesting

If you're not using CSS nesting, prefix each rule with the attribute selector:

```css
html[data-ogplus] nav { display: none; }
html[data-ogplus] footer { display: none; }
html[data-ogplus] .hero { padding: 60px; }
html[data-ogplus="twitter"] .hero { background: #1da1f2; }
```

## Tailwind CSS

Install the OpenGraph+ Tailwind plugin for `ogplus:` variants:

```sh
npm install @opengraphplus/tailwind
```

### Tailwind 4

```css
@import "tailwindcss";
@plugin "@opengraphplus/tailwind";
```

### Tailwind 3

```js
// tailwind.config.js
module.exports = {
  plugins: [require('@opengraphplus/tailwind')],
}
```

### Usage

```html
<!-- Hide in social cards -->
<nav class="ogplus:hidden">...</nav>

<!-- Show only in social cards -->
<div class="hidden ogplus:block ogplus:p-8">
  Social card content
</div>

<!-- Consumer-specific -->
<div class="ogplus:p-8 ogplus-linkedin:p-12 ogplus-twitter:bg-sky-500">
  Platform-optimized content
</div>
```

## Available variants

| Variant | Matches |
|---------|---------|
| `ogplus:` | All OpenGraph+ renders |
| `ogplus-linkedin:` | LinkedIn |
| `ogplus-twitter:` | Twitter/X |
| `ogplus-facebook:` | Facebook |
| `ogplus-discord:` | Discord |
| `ogplus-whatsapp:` | WhatsApp |
| `ogplus-apple:` | Apple Messages |
| `ogplus-bluesky:` | Bluesky |


## Hosted Stylesheet

A simple stylesheet with utility classes for showing and hiding elements in social card screenshots. For custom styling, see [Data Attributes](/docs/html-css/data-attributes).

## Installation

Link directly to the hosted stylesheet. No npm, no build step.

```html
<link rel="stylesheet" href="https://opengraphplus.com/stylesheet/v1.css">
```

## Usage

Add classes to any element to control its visibility in social card screenshots.

### Hide elements in social cards

Remove navigation, footers, cookie banners, and other clutter from your social cards.

```html
<nav class="ogplus-hide">Navigation hidden in screenshots</nav>
<footer class="ogplus-hide">Footer hidden in screenshots</footer>
```

### Show elements only in social cards

Create content that's invisible to regular visitors but appears in screenshots—like a custom hero or call-to-action designed specifically for social sharing.

```html
<div class="ogplus-show">
  This content is hidden on your site but appears in social card screenshots.
</div>
```

### Platform-specific visibility

Target individual platforms. Use `ogplus-{platform}-hide` or `ogplus-{platform}-show` where platform is `twitter`, `linkedin`, `facebook`, `discord`, `whatsapp`, `apple`, or `bluesky`.

```html
<!-- Hide only on Twitter -->
<div class="ogplus-twitter-hide">Hidden on Twitter cards</div>

<!-- Show only on LinkedIn -->
<div class="ogplus-linkedin-show">LinkedIn-specific content</div>
```

## Custom styles

Need more than show/hide? Write your own CSS targeting the `data-ogplus` attribute. See [Data Attributes](/docs/html-css/data-attributes) for details.


## Templates

Templates let you build completely custom social card layouts. Instead of styling your existing page, you define a new layout that extracts content from your page using JavaScript expressions.

## How templates work

Add a `<template>` element with the id `ogplus` to your page:

```html
<template id="ogplus">
  <div style="padding: 48px; background: #1a1a2e; color: white; height: 100%;">
    <h1 style="font-size: 48px; margin: 0;">
      ${document.querySelector('h1')?.textContent}
    </h1>
    <p style="font-size: 24px; color: #888; margin-top: 16px;">
      ${document.querySelector('meta[name="description"]')?.content}
    </p>
  </div>
</template>
```

When OpenGraph+ renders your page, it:

1. Finds the `<template id="ogplus">` element
2. Evaluates any `${...}` expressions against your page's DOM
3. Replaces the page body with the rendered template
4. Captures the result as your social card image

The template content is invisible to regular visitors—`<template>` elements don't render in the browser.

## Template expressions

Use `${...}` to insert dynamic content. Any valid JavaScript expression works:

```html
<template id="ogplus">
  <!-- Text content -->
  <h1>${document.querySelector('h1')?.textContent}</h1>
  
  <!-- Attribute values -->
  <img src="${document.querySelector('.hero img')?.src}">
  
  <!-- Meta tags -->
  <p>${document.querySelector('meta[property="og:description"]')?.content}</p>
  
  <!-- Computed values -->
  <span>${new Date().toLocaleDateString()}</span>
  
  <!-- Conditional content -->
  <span>${document.querySelector('.author')?.textContent || 'Anonymous'}</span>
</template>
```

Use optional chaining (`?.`) to safely handle missing elements without errors.

## Platform-specific templates

Create different layouts for different social platforms using `ogplus-{platform}` ids:

```html
<!-- Twitter-specific template -->
<template id="ogplus-twitter">
  <div style="background: #15202b; color: white; padding: 40px;">
    <h1>${document.querySelector('h1')?.textContent}</h1>
  </div>
</template>

<!-- LinkedIn-specific template -->
<template id="ogplus-linkedin">
  <div style="background: #0077b5; color: white; padding: 60px;">
    <h1>${document.querySelector('h1')?.textContent}</h1>
  </div>
</template>

<!-- Fallback for other platforms -->
<template id="ogplus">
  <div style="background: white; padding: 40px;">
    <h1>${document.querySelector('h1')?.textContent}</h1>
  </div>
</template>
```

OpenGraph+ checks for templates in this order:

1. `template#ogplus-{platform}` (e.g., `template#ogplus-twitter`)
2. `template#ogplus` (generic fallback)

If no template is found, OpenGraph+ falls back to rendering your page normally (with CSS styling via `data-ogplus`).

## Inline styles

Templates render in isolation—your page's stylesheets don't apply. Use inline styles for all formatting:

```html
<template id="ogplus">
  <div style="
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    height: 100%;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    font-family: system-ui, sans-serif;
  ">
    <h1 style="font-size: 64px; margin: 0; text-align: center;">
      ${document.querySelector('h1')?.textContent}
    </h1>
  </div>
</template>
```

## Full viewport layout

Your template fills the entire social card viewport. Use `height: 100%` on your root element to take up the full space:

```html
<template id="ogplus">
  <div style="
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    background: #f5f5f0;
  ">
    <!-- Your content -->
  </div>
</template>
```

## Grabbing existing elements

Instead of rebuilding your layout from scratch, you can grab and restyle existing elements from your page:

```html
<template id="ogplus">
  <div style="padding: 40px; background: white; height: 100%;">
    ${document.querySelector('.article-header')?.outerHTML}
  </div>
</template>
```

This clones the `.article-header` element with all its children. Note that styles may not carry over if they depend on parent selectors or external stylesheets.

## Example: Blog post card

A complete example for blog posts:

```html
<template id="ogplus">
  <div style="
    display: flex;
    flex-direction: column;
    padding: 48px;
    background: #0f172a;
    color: white;
    font-family: system-ui, sans-serif;
    height: 100%;
    box-sizing: border-box;
  ">
    <div style="
      font-size: 14px;
      text-transform: uppercase;
      letter-spacing: 2px;
      color: #94a3b8;
      margin-bottom: 24px;
    ">
      ${document.querySelector('.category')?.textContent || 'Blog'}
    </div>
    
    <h1 style="
      font-size: 56px;
      font-weight: bold;
      line-height: 1.1;
      margin: 0 0 24px 0;
      flex: 1;
    ">
      ${document.querySelector('h1')?.textContent}
    </h1>
    
    <div style="
      display: flex;
      align-items: center;
      gap: 16px;
      font-size: 18px;
      color: #94a3b8;
    ">
      <img 
        src="${document.querySelector('.author-avatar')?.src}" 
        style="width: 48px; height: 48px; border-radius: 50%;"
      >
      <span>${document.querySelector('.author-name')?.textContent}</span>
      <span>·</span>
      <span>${document.querySelector('.publish-date')?.textContent}</span>
    </div>
  </div>
</template>
```

## Example: Product card

For e-commerce or product pages:

```html
<template id="ogplus">
  <div style="
    display: flex;
    height: 100%;
    background: white;
    font-family: system-ui, sans-serif;
  ">
    <img 
      src="${document.querySelector('.product-image')?.src}"
      style="width: 50%; height: 100%; object-fit: cover;"
    >
    <div style="
      flex: 1;
      padding: 48px;
      display: flex;
      flex-direction: column;
      justify-content: center;
    ">
      <h1 style="font-size: 36px; margin: 0 0 16px 0;">
        ${document.querySelector('.product-name')?.textContent}
      </h1>
      <p style="font-size: 24px; color: #16a34a; font-weight: bold; margin: 0;">
        ${document.querySelector('.product-price')?.textContent}
      </p>
    </div>
  </div>
</template>
```

## Combining with meta tags

Templates work alongside other OpenGraph+ meta tags:

```html
<head>
  <!-- Set viewport width for template rendering -->
  <meta property="og:plus:viewport:width" content="800">
  
  <!-- Add extra styles to the body -->
  <meta property="og:plus:style" content="margin: 0; padding: 0;">
</head>

<body>
  <template id="ogplus">
    <!-- Your template -->
  </template>
</body>
```

## Templates vs CSS styling

| Feature | CSS Styling | Templates |
|---------|-------------|-----------|
| Complexity | Simple show/hide | Full custom layouts |
| Styles | Uses your existing CSS | Inline styles only |
| Layout | Adapts your page | Completely custom |
| Content | Shows/hides existing elements | Extracts and rearranges content |
| Best for | Minor adjustments | Branded social cards |

Use CSS styling when you just need to hide navigation and adjust spacing. Use templates when you want a completely different layout for social cards.


## HTTP Caching

OpenGraph+ reads the HTTP cache headers from your page to figure out when to re-render social card images. If you already have caching set up, it just works.

## How it works

When OpenGraph+ fetches your page, it checks the response headers:

1. **First request**: OpenGraph+ renders your page and stores the cache headers
2. **Subsequent requests**: If the cached image is still fresh, it's served immediately
3. **Expired cache**: OpenGraph+ re-fetches with conditional headers and re-renders if content changed

## Cache-Control

The `Cache-Control` header is the main way to control caching. OpenGraph+ looks at `max-age` to know how long an image stays fresh.

```http
Cache-Control: max-age=3600
```

This tells OpenGraph+ the image is good for 1 hour (3600 seconds). During that window, requests get the cached image without hitting your server.

### Common values

| Header | Behavior |
|--------|----------|
| `max-age=86400` | Fresh for 24 hours |
| `max-age=604800` | Fresh for 1 week |
| `max-age=2592000` | Fresh for 30 days |
| `no-cache` | Always revalidate (but can use ETag) |
| `no-store` | Never cache, always re-render |

### Private and no-store

If your page returns `Cache-Control: private` or `Cache-Control: no-store`, OpenGraph+ re-renders on every request. Use this for pages with user-specific content that shouldn't be cached.

## ETags

ETags let you revalidate efficiently. When your page includes an `ETag` header, OpenGraph+ stores it and sends it back on the next request as `If-None-Match`.

```http
# Your server's response
ETag: "abc123"

# OpenGraph+'s next request
If-None-Match: "abc123"
```

If your content hasn't changed, your server returns `304 Not Modified` and OpenGraph+ serves the cached image without re-rendering.

## Last-Modified

Like ETags, the `Last-Modified` header enables conditional requests. OpenGraph+ sends `If-Modified-Since` on subsequent requests.

```http
# Your server's response
Last-Modified: Wed, 21 Jan 2026 10:00:00 GMT

# OpenGraph+'s next request
If-Modified-Since: Wed, 21 Jan 2026 10:00:00 GMT
```

If your page hasn't changed since that time, return `304 Not Modified`.

## Expires

The `Expires` header sets an absolute expiration time. OpenGraph+ uses this as a fallback when `Cache-Control: max-age` isn't present.

```http
Expires: Wed, 22 Jan 2026 10:00:00 GMT
```

Prefer `Cache-Control: max-age` over `Expires` for more predictable behavior.

## Meta tag overrides

If you can't control HTTP headers, use meta tags to set cache behavior directly in your HTML. These take priority over HTTP headers.

### Cache max age

Set how long the image stays fresh (in seconds):

```html
<meta property="og:plus:cache:max_age" content="3600">
```

This works exactly like `Cache-Control: max-age=3600`.

### Cache ETag

Set a version identifier for your content:

```html
<meta property="og:plus:cache:etag" content="v1.2.3">
```

When the etag changes, OpenGraph+ re-renders even if the cache hasn't expired. Use this to force updates when you publish new content.

### Example

```html
<head>
  <meta property="og:plus:cache:max_age" content="86400">
  <meta property="og:plus:cache:etag" content="post-123-rev-5">
</head>
```

This caches the image for 24 hours, but re-renders immediately if you change the etag to `post-123-rev-6`.

### Priority order

OpenGraph+ uses this priority for cache settings:

1. **Site-level TTL** (configured in your dashboard)
2. **Meta tags** (`og:plus:cache:max_age`, `og:plus:cache:etag`)
3. **HTTP headers** (`Cache-Control`, `ETag`, `Expires`)

## Default behavior

If your page returns no cache headers, OpenGraph+ defaults to a 30-day TTL. This prevents excessive re-rendering while keeping images reasonably fresh.

## Framework examples

Most frameworks make it easy to set cache headers.

### Static file servers

Nginx:
```nginx
location / {
  expires 7d;
  add_header Cache-Control "public, max-age=604800";
}
```

### Node.js / Express

```js
app.get('/page', (req, res) => {
  res.set('Cache-Control', 'public, max-age=86400');
  res.render('page');
});
```

### PHP

```php
header('Cache-Control: public, max-age=86400');
```

### Python / Django

```python
from django.views.decorators.cache import cache_control

@cache_control(max_age=86400, public=True)
def page(request):
    return render(request, 'page.html')
```

For Rails-specific caching with ETags, see the [Rails Caching](/docs/rails/caching) guide.

## Recommendations

- **Static pages**: Long TTLs (7-30 days) since content rarely changes
- **Blog posts**: Medium TTLs (1-7 days) with ETags for efficient updates
- **Dynamic pages**: Short TTLs (1-24 hours) or `no-cache` with ETags
- **User-specific pages**: `no-store` to prevent caching entirely


## Preview Bookmarklet

The OG+ Preview bookmarklet simulates the rendering environment in your browser so you can test `ogplus:` styles locally.

[Get the bookmarklet](/tools/bookmarklet)

## Usage

1. Navigate to any page on your site
2. Click the bookmarklet to open the consumer selection modal
3. Select a consumer (LinkedIn, Twitter, Facebook, etc.) to preview those styles
4. Press **Esc** to exit preview mode and remove the attribute

The bookmarklet sets `data-ogplus` on your `<html>` element, triggering your `ogplus:` styles.

## What to check

When previewing, verify:

- Navigation and footers hide correctly
- Social card content appears
- Padding and spacing look right per consumer
- Text is readable at the rendered size

## Troubleshooting

**Nothing changes?** Make sure you have `ogplus:` variants defined in your CSS.

