This article is a part of larger compendium on content types in Powershell, comprising several articles:
SharePoint Online content types in Powershell: Get
SharePoint Online content types in Powershell: Edit




Content types were introduced in SharePoint 2007 products and technologies.[1] A content type defines the metadata and behavior for a particular data entity—usually, a business document or item of some kind. Each content type contains references to one or more site columns. You can also associate workflows, information management policies, and document templates with content types. A SharePoint content type pulls together an item and information about the item. It makes it easy to provide consistency across a site. Some of the popular content types are item or a document. A site collection owner can create custom content types that would reflect more the needs of a company, and add it to selected lists and libraries. A content type can be also published from a content type hub and managed centrally.

Powershell


All those actions can be performed through User Interface as well as via Powershell using SharePoint Online SDK. Below you will find steps for creating, adding, searching, and removing the content types. Full list of all ready, published, and free scripts you will find at the bottom of the article here. All the scripts have been also re-written into Powershell cmdlets and packaged into a SharePoint Online Content Types Module available here.

Add Content Type


As with creating lists or libraries, creating a content type involves using CreationInformation, in this case ContentTypeCreationInformation class.

$lci =New-Object Microsoft.SharePoint.Client.ContentTypeCreationInformation

Properties


Its properties include Description, Group, ID, Name, ParentContentType, and TypeID which is reserved for internal use and should not be used within the code. Name property is required to be defined before the CreationInformation variable is used for creating the content type.

Description Optional Type: System.String
Group Optional Type: System.String
ID Unique Type: System.String
Name Mandatory Type: System.String
ParentContentType Optional Type: Microsoft.SharePoint.Client.ContentType
TypeID Internal Type: System.String


Group


Group property specifies a site content type group. Categorizing content types into groups makes it easier for users to find them. It corresponds to the drop down box in the image below and should have a format similar to:
"List Content Types"
"Group Work Content Types"



Current content type groups include:
Business Intelligence
Community Content Types
Custom Content Types
Digital Asset Content Types
Display Template Content Types
Document Content Types
Document Set Content Types
Folder Content Types
Group Work Content Types
List Content Types
Page Layout Content Types
Publishing Content Types
Special Content Types


The Group does not decide whether the list content type is available for adding to a specific library or list. E.g.  List Content Type Group is not available for library, because it does not include any compatible content types. If we add a custom content type derived from Document Parent Content Type, the group with a single member (the incompatible ones will be ignored) will appear in the selection:



                                           Remember!                                    
 Under Add Content Types only content types compatibleo o
             with the list/library are displayed for selection            

If the group is not specified, the content type will be added to Custom Content Types by default.

Content Type ID


Content type IDs uniquely identify the content type and are designed to be recursive. The content type ID encapsulates that content type's lineage, or the line of parent content types from which the content type inherits. Each content type ID contains the ID of the parent content type, which in turn contains the ID of that content type's parent, and so on, ultimately back to and including the System content type ID. SharePoint Foundation uses this information to determine the relationship between content types and for push-down operations.[1]

You can construct a valid content type ID using one of two conventions:

  • Parent content type ID + two hexadecimal values (the two hexadecimal values cannot be "00")
  • Parent content type ID + "00" + hexadecimal GUID

Source: https://msdn.microsoft.com/en-us/library/office/aa543822%28v=office.14%29.aspx?f=255&MSPPError=-2147217396

An ID of a custom content type based on Item, will look like this when using GUID approach:
0x01 which is an Item Content Type ID
00   
9e862727eed04408b2599b25356e7914 the hexadecimal value


Item Content Type ID following the convention  the hexadecimal value 
0x01 00    9e862727eed04408b2599b25356e7914

Altogether: 0x01009e862727eed04408b2599b25356e7914
 
You can create a guid automatically using Windows Powershell 
[System.Guid]::NewGuid().toString()
or
[guid]::NewGuid()

You can also invent your own and the following script will be using 0x0100aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa which also works :)

Each content type ID must be unique within a site collection. If the script does not specify the guid, SharePoint will assign one to the content type.

ParentContentType


Parent Content Type specifies the parent content type for the content type that will be constructed. The parameter can use both get; and set; methods. ParentContentType parameter is mutually exclusive with the ID parameter. It is due to the information encoded in the ID. ID parameter itself includes the parent content type in its first characters (e.g. "0x01" for the item). So what happens if I will try to create a content type with both those parameters specified?

$lci.ID="0x0108009e862727eed04408b2599b25356e7914"
$lci.ParentContentType=$ctx.Web.ContentTypes.GetById("0x01")

"Parameters.Id, parameters.ParentContentType cannot be used together. Please only use one of them."



To a Site Collection


Script


Having established what information is needed for creating a content type, open Powershell ISE (or a NotePad, that's also an option, though not a very convenient one).

1. Enter paths to SharePoint Online SDK Client and Client.Runtime libraries installed on your local computer.

Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"
Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"

2. Create a Microsoft.SharePoint.Client.ClientContext. $Username, $Url and $AdminPassword variables were defined earlier. ExceuteQuery() is not required at this stage. It is a personal preference and allows me to eliminate incorrect credentials as potential errors at an early stage.

$ctx=New-Object Microsoft.SharePoint.Client.ClientContext($Url)
$ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($Username$AdminPassword)
$ctx.ExecuteQuery()

3. Define the ContentTypeCreationInformation

$lci =New-Object Microsoft.SharePoint.Client.ContentTypeCreationInformation
  $lci.Description="Description"
  $lci.Name="Powershell Content Type2"
  $lci.ParentContentType=$ctx.Web.ContentTypes.GetById("0x01")
  $lci.Group="List Content Types"

or
$lci =New-Object Microsoft.SharePoint.Client.ContentTypeCreationInformation
#$lci.Description="Description"
$lci.Name="Powershell Content Type2id22aa"
#$lci.Group="List Content Types"
$lci.ID="0x0100aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

# marks Powershell in-line comment. Non-mandatory parameters Description and Group are commented out here. It is up to you, to define and use them or rely on default settings.

4. Using the CreationInformation, add the content type to site content types.
$ContentType $ctx.Web.ContentTypes.Add($lci)
$ctx.Load($contentType)
$ctx.ExecuteQuery()

5. Error handling:

$ContentType $ctx.Web.ContentTypes.Add($lci)
$ctx.Load($contentType)
try
   {
      
       $ctx.ExecuteQuery()
       Write-Host "Content Type " $Title " has been added to " $Url
   }
   catch [Net.WebException]
   {
      Write-Host $_.Exception.ToString()
   }

Full script


The above script has been packaged into a function (functions are easier to re-use, among other things). Below you can find the full script. Feel free to use it but please acknowledge my contribution.

function New-SPOContentType
{
param(
[Parameter(Mandatory=$true,Position=1)]
[string]$Username,
[Parameter(Mandatory=$true,Position=2)]
$AdminPassword,
        [Parameter(Mandatory=$true,Position=3)]
[string]$Url
)
   
  $ctx=New-Object Microsoft.SharePoint.Client.ClientContext($Url)
  $ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($Username$AdminPassword)
 
  $ctx.ExecuteQuery()
 
   
 
  $lci =New-Object Microsoft.SharePoint.Client.ContentTypeCreationInformation
  $lci.Description="Description"
  $lci.Name="Powershell Content Type2"
  $lci.ParentContentType=$ctx.Web.ContentTypes.GetById("0x01")
  $lci.Group="List Content Types"
   
  $ContentType $ctx.Web.ContentTypes.Add($lci)
  $ctx.Load($contentType)
  try
     {
        
         $ctx.ExecuteQuery()
         Write-Host "Content Type " $Title " has been added to " $Url
     }
     catch [Net.WebException]
     {
        Write-Host $_.Exception.ToString()
     }
 
      
 
}
 
 
 
  # Paths to SDK. Please verify location on your computer.
Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"
Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
 
# Insert the credentials and the name of the admin site
$Username="admin@tenant.onmicrosoft.com"
$AdminPassword=Read-Host -Prompt "Password" -AsSecureString
 
 
New-SPOContentType -Username $Username -AdminPassword $AdminPassword -Url $AdminUrl


The script can also be downloaded from the Technet Gallery.

To Content Hub


Content hub is a feature in SharePoint Online to manage content types centrally and publish them to the subscribed sites. It provides a central location for unified management of content types. From the content type hub you can publish a given content type across multiple site collections. The central location ensures that the settings, columns, and look of the content type is consistent across the whole organization. The content types appear in the subscribing sites under Site Settings>Content Type Publishing:



To compare, in the previous example, having added a content type to a site collection scope, you can propagate it only to underlying lists. Adding the content type to a content type hub will allow you to use the same content type across your whole tenant.

How to find content hub?

From site collection level

  1. Select the Options button Settings button and then select Site Settings.

  2. Under Site Collection Administration, click Content type publishing.

  3. In the Hubs section, you can see the names of any Managed Metadata Service applications that publish content types to this site collection listed in bold text. After the service application names, you can see the URLs for the hub sites. You can also see a list of the subscribed content types.


Direct url


https://TENANT.sharepoint.com/sites/contenttypehub

where TENANT is the name of your SharePoint Online tenant.

Script


The ContentTypeCreationInformation and its properties undergo the same considerations as when adding it to a site collection level. As can be seen by the url above, in many respects the content type hub is just another site collection. The full script adding a content type to CTH differs only through its url:

function New-SPOContentType
{
param(
[Parameter(Mandatory=$true,Position=1)]
        [string]$Username,
        [Parameter(Mandatory=$true,Position=2)]
        $AdminPassword,
        [Parameter(Mandatory=$true,Position=3)]
        [string]$Url,
[Parameter(Mandatory=$true,Position=4)]
        [string]$Description,
[Parameter(Mandatory=$true,Position=5)]
        [string]$Name,
[Parameter(Mandatory=$true,Position=6)]
        [string]$Group,
[Parameter(Mandatory=$true,Position=7)]
        [string]$ParentContentTypeID
 
        )
   
  $ctx=New-Object Microsoft.SharePoint.Client.ClientContext($Url)
  $ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($Username$AdminPassword)
 
  $ctx.ExecuteQuery()
 
   
 
  $lci =New-Object Microsoft.SharePoint.Client.ContentTypeCreationInformation
  $lci.Description=$Description
  $lci.Name=$Name
  #$lci.ID="0x0108009e862727eed04408b2599b25356e7914"
  $lci.ParentContentType=$ctx.Web.ContentTypes.GetById($ParentContentTypeID)
  $lci.Group=$Group
   
  $ContentType $ctx.Web.ContentTypes.Add($lci)
  $ctx.Load($contentType)
  try
     {
        
         $ctx.ExecuteQuery()
         Write-Host "Content Type " $Name " has been added to " $Url
     }
     catch [Net.WebException]
     {
        Write-Host $_.Exception.ToString()
     }
 
      
 
}
 
 
 
  # Paths to SDK. Please verify location on your computer.
Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"
Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
 
# Insert the credentials and the name of the admin site
$Username="admin@tenant.onmicrosoft.com"
$AdminPassword=Read-Host -Prompt "Password" -AsSecureString
$Description="desc"
$Name="From Powershell directly to CTH"
$ParentContentTypeID="0x01"
$Group="List Content Types"
 
 
New-SPOContentType -Username $Username -AdminPassword $AdminPassword -Url $AdminUrl -Description $Description -Name $Name -Group $Group -ParentContentTypeID $ParentContentTypeID

The script can be downloaded from Technet Gallery here.



Directly to list


A content type can be created directly within a list content types and be available only in a this list's scope. It is a practical solution if the content type you are going to create is very specific only to this particular list and should not be used elsewhere.

Content Type Management


In order to see the added content types and be able to use them, ensure that the list has content type management enabled. 
This setting can also be changed via Powershell script and examples can be found in the scripts below:
Allow content type management for all lists in site collection using Powershell
Allow content type management for all lists in a site using Powershell
Set content type management setting for SharePoint Online list using Powershell
Custom Powershell cmdlet Set-SPOList -ContentTypesEnabled

Script


When it comes to adding a content type to list, there are several options, depending on the requirements. Using one script the content type can be added to:
  • a single list
  • all lists
  • several lists fulfilling common criteria

     A single list


In order to target the list, some information about the list is needed. For one specific list, the best are ID or Title. They allow to leverage GetByID andGetByTitle methods:

$ctx.Web.Lists.GetByID("4C834F99-D908-4E1C-A04F-C8B1F971371C")
or
$ctx.Web.Lists.GetByTitle("The Title of My List")

     All Lists


It is also possible to add the content type in all existing lists. In order to add the content type to all lists, you need to retrieve the list collection from the .Web and loop through each of the lists, adding the content type there 

$ctx.Load($ctx.Web.Lists)
  $ctx.ExecuteQuery()
  foreach($ll in $ctx.Web.Lists)
  {
    #Add Content Type here
  }

     
      Some Lists


 Once you have created your content type, you may decide that it is appropriate only for specific lists. There is no end to filter options. You can choose only announcement lists, only lists with content type enabled, lists with a certain content type already added, lists created after or before a certain date, lists with enabled versioning, or those that include certain fields or views. Below there are some of the examples:  
  • tasks lists
foreach($ll in $ctx.Web.Lists)
  {
 
   if($ll.BaseTemplate -eq 107 -or $ll.BaseTemplate -eq 171)
     {
          # Add Content Type comes here
     }
  }

  • lists with workflows
$ctx.Load($ctx.Web.Lists)
$ctx.ExecuteQuery()
 
  foreach($ll in $ctx.Web.Lists)
  {
 
     $ctx.Load($ll.WorkflowAssociations)
     $ctx.ExecuteQuery()
 
      if($ll.WorkflowAssociations.Count -gt 0)
       {
             # Add Content Type comes here
      
        }
  }

Full script


The script below is an example of adding a content type to lists with workflows. In order to get a script for other scenarios, substitute its sections with appropriate modifications. The scripts can be downloaded from Technet Gallery:
Add Content Type to Lists with Workflows 
Add Content Type to Task Lists
Create content type and add it to all lists in one site
Create content type and add directly to SPO list using Powershell

function New-SPOContentType
{
param(
[Parameter(Mandatory=$true,Position=1)]
        [string]$Username,
        [Parameter(Mandatory=$true,Position=2)]
        $AdminPassword,
        [Parameter(Mandatory=$true,Position=3)]
        [string]$Url,
[Parameter(Mandatory=$true,Position=4)]
        [string]$Description,
[Parameter(Mandatory=$true,Position=5)]
        [string]$Name,
[Parameter(Mandatory=$true,Position=6)]
        [string]$Group,
[Parameter(Mandatory=$true,Position=7)]
        [string]$ParentContentTypeID
 
        )
   
  $ctx=New-Object Microsoft.SharePoint.Client.ClientContext($Url)
  $ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($Username$AdminPassword)
  $ctx.Load($ctx.Web.Lists)
  $ctx.ExecuteQuery()
 
   
 
  $lci =New-Object Microsoft.SharePoint.Client.ContentTypeCreationInformation
  $lci.Description=$Description
  $lci.Name=$Name
  #$lci.ID="0x0100aa862727aed04408b2599b25356e7000"
  $lci.ParentContentType=$ctx.Web.ContentTypes.GetById($ParentContentTypeID)
  $lci.Group=$Group
   
  foreach($ll in $ctx.Web.Lists)
  {
 
     $ctx.Load($ll.WorkflowAssociations)
     $ctx.ExecuteQuery()
 
   if($ll.WorkflowAssociations.Count -gt 0)
   {
  $ContentType $ll.ContentTypes.Add($lci)
  $ctx.Load($contentType)
  try
     {
        
         $ctx.ExecuteQuery()
         Write-Host "Adding content type " $Name " to " $ll.Title
     }
     catch [Net.WebException]
     {
        Write-Host $_.Exception.ToString()
     }
 
      
     }
     }
}
 
 
 
  # Paths to SDK. Please verify location on your computer.
Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"
Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
 
# Insert the credentials and the name of the admin site
$Username="admin@tenant.onmicrosoft.com"
$AdminPassword=Read-Host -Prompt "Password" -AsSecureString
$Description="desc"
$Name="From PS to Tasks234"
$ParentContentTypeID="0x0107"
$Group="List Content Types"
 
 
 
New-SPOContentType -Username $Username -AdminPassword $AdminPassword -Url $AdminUrl -Description $Description -Name $Name -Group $Group -ParentContentTypeID$ParentContentTypeID

From an existing content type


Once you have added a content type to a site collection or to a Content Type Hub and it has propagated to subscribing sites, it can be also added to a list as an existing content type. Such solution allows centralized control over the settings of the content type. You can apply changes at a site collection level and then update all content types:



or you can edit the content type in the Content Type Hub and republish it in order to propagate the changes to all subscribing sites. The changes may take up to 48 hours





Script


There is a designated method for adding a content type which already exists at a site collection level. The method is called AddExistingContentType and belongs toContentTypeCollection class.  The method returns a ContentType object. 

1. Create a context.

$ctx=New-Object Microsoft.SharePoint.Client.ClientContext($Url)
$ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($Username$AdminPassword)
$ctx.Load($ctx.Web.Lists)
$ctx.ExecuteQuery()

2. Get the Content type you want to add using its ID.

$contentType=$ctx.Web.ContentTypes.GetById($ContentTypeID)
$ctx.Load($contentType)

3. Retrieve the list and the content type collection associated with that list:

$ll=$ctx.Web.Lists.GetByTitle($ListTitle)
$ctx.load($ll)
$ctx.load($ll.ContentTypes)
$ctx.ExecuteQuery()

4. Make sure the Content Type Management is allowed.

$ll.ContentTypesEnabled=$true

5. Add the content type.  

$AddedContentType=$ll.ContentTypes.AddExistingContentType($contentType)
$ll.Update()

The method returns a content type, so running the method alone 

$ll.ContentTypes.AddExistingContentType($contentType)

will return "Collection has not been initialized" error as described brilliantly here.



6. Execute the request and add basic error handling.

try
     {
        
         $ctx.ExecuteQuery()
         Write-Host "Adding content type " $AddedContentType.Name " to " $ll.Title
     }
catch [Net.WebException]
     {
        Write-Host $_.Exception.ToString()
     }

Full script


 The above described commands have been packaged into a function and script available for download here: Add existing content type directly to SPO list using Powershell

function Add-SPOContentType
{
param(
[Parameter(Mandatory=$true,Position=1)]
        [string]$Username,
        [Parameter(Mandatory=$true,Position=2)]
        $AdminPassword,
        [Parameter(Mandatory=$true,Position=3)]
        [string]$Url,
        [Parameter(Mandatory=$true,Position=4)]
        [string]$ListTitle,
[Parameter(Mandatory=$true,Position=7)]
        [string]$ContentTypeID
 
        )
   
  $ctx=New-Object Microsoft.SharePoint.Client.ClientContext($Url)
  $ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($Username$AdminPassword)
  $ctx.Load($ctx.Web.Lists)
  $ctx.ExecuteQuery()
 
  $contentType=$ctx.Web.ContentTypes.GetById($ContentTypeID)
  $ctx.Load($contentType)
 
 $ll=$ctx.Web.Lists.GetByTitle($ListTitle)
 $ctx.load($ll)
 $ctx.load($ll.ContentTypes)
 $ctx.ExecuteQuery()
  $ll.ContentTypesEnabled=$true
 $AddedContentType=$ll.ContentTypes.AddExistingContentType($contentType)
 $ll.Update()
  try
     {
        
         $ctx.ExecuteQuery()
         Write-Host "Adding content type " $AddedContentType.Name " to " $ll.Title
     }
     catch [Net.WebException]
     {
        Write-Host $_.Exception.ToString()
     }
 
      
      
}
 
 
 
  # Paths to SDK. Please verify location on your computer.
Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"
Add-Type -Path "c:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.Runtime.dll"
 
# Insert the credentials and the name of the admin site
$Username="admin@tenant.onmicrosoft.com"
$AdminPassword=Read-Host -Prompt "Password" -AsSecureString
$ListTitle="tas1207"
$ContentTypeID="0x01200200C44754774BD8D4449F4B7E3FE70A7E0E"
 
 
 
Add-SPOContentType -Username $Username -AdminPassword $AdminPassword -Url $AdminUrl -ListTitle $ListTitle -ContentTypeID $ContentTypeID



References


Articles


This article has spanned and is integrally connected with several other articles. For more information please look at:
SharePoint Online content types in Powershell: Add
SharePoint Online content types in Powershell: Get

SharePoint Online content types in Powershell: Group property
SharePoint Online content types in Powershell: Fields vs Field Links
SharePoint Online content types in Powershell: Known Errors



Downloads


Several (better or worse) scripts originated from this article. Most of them are available for download from Technet Gallery and are waiting for your comments there.

 Adding

SharePoint Online: Create a content type using Powershell
Create content type and add directly to SPO list using Powershell
Create and add content type to a content type hub SharePoint Online
Create content type and add it to all lists in one site
Add Content Type to Task Lists
Add Content Type to Lists with Workflows
Add existing content type directly to SPO list using Powershell

Getting

Get all properties of all content types in a SharePoint site
Get All Properties of All Content Types (Detailed)
Get All Properties of All Content Types in All Lists (Detailed) across one site

Get properties of a single content type by its ID
Get content types belonging to a group
Get All Hidden Content Types added to the site
Get Single Content Type - Array Method
Get Names of all content types added to your SPO lists
Get Names of All Content Types
Get Names of all Available Content Types
Get Content Types Derived From One Parent
Get Content Types Derived From One Parent 2 
Get content types which cannot be modified
Get Content Types with a particular column
SharePoint Online: Get the custom form display url using Powershell
Find Custom Display Forms Deployed on your lists using Powershell
Verify the content types used in your lists against the default ones
Retrieve all Content Types from a Content Type Hub and their DisplayFormTemplate


Editing

"Unseal" sealed content types in SharePoint Online site collection
Modify the description of a list content type using Powershell
Modify the description of a site content type using Powershell
Unable to view items: modify the DisplayFormUrl back to default one
Modify the Display Form Template Name
Reset default content types
Add column (fieldlink) to a content type using Powershell
Assign your Content Types back to their default Groups using Powershell


 Content Type Management

Allow content type management for all lists in site collection using Powershell
Allow content type management for all lists in a site using Powershell
Set content type management setting for SharePoint Online list using Powershell
Custom Powershell cmdlet Set-SPOList -ContentTypesEnabled



Related Scripts

SharePoint Online: Check content types added to your lists
SharePoint Online: Check content types added to your lists (recursive)
Get a report on lists where a given content type is added
SharePoint Online: Remove a content type from all lists in a site collection
Compare Web.AvailableContentTypes vs Web.ContentTypes

The original article has been posted by me here.



Other Languages


Powershell i typy zawartości SharePoint Online: Dodawanie (pl-PL)



free hit counter